The Ur-Language is probably verilog, or at least the raw instructions of the platform; and C isn't much closer to those instructions than many of its alternatives, thanks to optimizing compilers and the differences between the true form of the computational model of the platform and the imaginary one that the C standard reflects.
I've been writing a large app in C again after a few years away. Originally wrote the app in C++ then again in Python and now in straight C. I enjoy the faster compile times of C (over C++) and the better type checking of C (over Python).
However, when I'm carefully using C++, I don't have to fiddle around with memory management and still get fast performance and better type checking.
Yesterday I wrote a Python script to generate C code test cases. String manipulation in Python is very easy.
This [1] explains how to create them using nested functions. Near the end is a quick blurb about the implementation which is probably why c++ doesn’t do it that way.
I use them. The executable stack bit doesn't bother me since I'm on an ARM Cortex. Would be nice if they fixed that because they are kinda handy once you get used to them.
It's what it doesn't give me. Doesn't have a memory management unit, so no stack execution protection. So no protection against classic stack smashing attacks.
I saw a talk about weird machines. The upshot of it is if you can clobber the return address on the stack you can usually create a weird machine that you can then program[1]. Leads me to believe that if you are putting entrusted data and return addresses on the same stack you're in a state of sin security wise.
[1] Also saw an exploit on an embedded system where they leveraged that even though the memory was locked you could set the program counter and read/write registers via jtag. Using that they were able in short order to recover the devices security keys and re-flash it with their own code.
I don’t like C++ anymore. I used to love it but now it’s just a bloated mess of a language. I get it, you don’t have to use every part of the language but I really don’t like any of the newer bits and that’s what everyone wants to use.
It's pretty simple. I'm decoding a structure into bit fields (802.11 Information Element). I have a hex dump that I decode into a two structures with almost 100 fields (eyeroll for 802.11 committee). Each field can only be 0, 1, or sometimes up to 15.
I have a list of the structure members. I generate an assert for each member being a certain value. The python script builds/runs the C code. On failure, I parse the assertion failure, get the actual value, change the C assert string, rebuild-rerun. Continue until the program succeeds.
I've visually verified the decode is correct once. I want to keep the decoder tested if I change the code again so I'm generating complete test coverage.
It sounds like your use case is property based testing. Have you tried any of the QuickCheck ports to C++? My favorite is RapidCheck (https://github.com/emil-e/rapidcheck). You define generators for your struct fields and write a pure function to check whether the parsed output conforms to your expectations.
Note that Python's official type checker (mypy) is pretty good (within the limits of type erasure); I started the project I've been leading up at work this past year in statically-typed Python, and it's been a great experience.
I haven't tried mypy yet but want to get to it. It sounds very interesting! After using Python so much the last several years, I've gotten more curmudgeonly about strong type checking in my programming languages.
Don't wait for a new project, just start using it (is most beneficial if for every function you specify types for parameters and output). If you use an IDE that understand it like PyCharm, suddenly the autocomplete will start working right, so will refactoring and it will start highlighting potential errors.
I tried to like mypy, really did. But it always ended up in some kind of circular import tailspin for me. C++ gets around the problem by allowing separation of interface and implementation, Haskell by allowing circular imports. Maybe I missed something, maybe it's fixed by now, but that was my experience a couple of years ago.
I've played with mypy a little, and it's certainly not bad, but it does suffer by comparison to typescript, which just has a lot of really great structural typing features that mypy still lacks.
mypy recently got structural typing. They call it Protocols, which isn't the first thing one one put into google when looking for this feature. It's not as powerful and terse as typescript unfortunately.
> Yesterday I wrote a Python script to generate C code test cases.
I do this quite a bit. Basically, anytime there is something very boiler-platy (like HTTP APIs, configuration management, test cases, etc.) I write metadata in JSON that describes what I'm adding, and then at build time a python script will parse the metadata and generate all of the C or C++ which is then compiled. I even have the python generating comments in the generated C/C++!
I used to use Jinja2 for generating the C/C++ files, but now I just use f-strings in the latest version of vanilla Python3.
This is really interesting — nice work! Have you published any tooling as OSS or examples? If you haven’t seen it before, the Model Oriented Programming work by Pieter Hintjens is worth looking at.
Not yet. Most of the stuff I've done is internal to my company unfortunately. But just to give you an idea, if I were adding a new configuration parameter to our IoT device, I would add an object to a JSON array that looks like this:
{
"name": "backoff_mode",
"group": "telemetry",
"description": "We don't want to clog up networks with tons of failing attempts to send home telemetry. This parameter configures the device to back off attempts according to the specified mode",
"cpptype": "std::string",
"enum": ["none", "linear", "exponential"],
"default_value": "exponential"
}
At build time, a few things are generated, including a .h file with the following:
The boilerplate for getting and setting this parameter is also generated. Thus, just by adding that simple piece of metadata, all the boilerplate and documentation is generated and you as the developer can focus on the actual logic that needs to be implemented regarding this parameter.
This is what Lisp/Scheme based languages use macros for -> to reduce boilerplate code. A lot of programmers hate DSL's, but using Python to generate test code is the same as using a DSL. The difference is, Python is a much bigger and more complicated DSL, than a test-macro will probably ever be.
There is nothing wrong with generating code in a pragmatic way. It is just strange to see all the time people using workarounds for things that a tiny DSL could probably solve better.
There's another advantage to using a high level language like python though - since the "source" is now JSON metadata which is decoupled from the code, I can use that metadata to generate other interesting things. For example, I can use python-docx to generate a Word document, for example (which my company requires for documentation tracking). Or, if the metadata is describing HTTP APIs, I can generate an OpenAPI document which can be fed into Swagger UI[1].
One large factor is that when somebody really dislike the result of a code generator, they most commonly cope by editing the generated code by hand. With other metaprogramming techniques people can't do the same, so they fix the issues.
The end result is that code generators are usually a set of mostly-functioning tools that create bad code that is almost, but not exactly impossible to understand but that you will be required to change at some point.
Code generation usually doesn’t get you any support from standard tooling. My autocomplete can see through macros, but it has no idea what C is if I was working with Perl.
In the mid-90's, before mod_perl, I used to generate PHP in minutely cron job using a Perl script from a constantly changing database. That came close to the best of both worlds at the time: the flexibility of Perl, fed from a database, and no CGI overhead when serving.
Thanks for the link! I just installed it on my i5 Thinkpad. I can't test filters and more complex stuff right now, but it loads super fast, like 150 ms uncached, and probably less than 50 ms when cached.
I'm curious if there is other software around rewritten in pure C. It would likely be a huge task not worth the effort, but I can't but imagine the speed and size gains by rewriting say Firefox or Libreoffice.
I think what squarefoot is looking for is lightweight (tiny?) C apps that have limited abstraction layers. For example, Gtk+, which is pure C, is no longer considered lightweight. It was with Gtk+ 1.2 because GTK was just a small wrapper around X11 in the 1.x days. That is what AzPainter is. AzPainter basically depends on xlib, xft/freetype, and some image libraries (libpng, libjpeg).
Modern C apps with a GUI typically build on GTK but that toolkit has become massive. Modern GTK3 apps are built on top of dbus, pango, atk, cairo. This is not necessarily a bad thing though and libraries like ATK provide accessibility which is important. At the time GTK+ was created the competing toolkit at the time, under Unix, was Motif and it had the nickname "bloatif". Now we've reached a point where GTK3 is much much larger and "bloated" than a 2019 Motif application.
If we include C++ the Qt and WxWidgets toolkits are also massive. FLTK is still a lightweight option though.
"I think what squarefoot is looking for is lightweight (tiny?) C apps that have limited abstraction layers. For example, Gtk+, which is pure C, is no longer considered lightweight."
Yes, I was also meaning the ui, which in this one is amazingly fast. I tried a few filters and they seem fast too, but being not a gfx expert I have no way to judge. But the ui is incredible; same speed on my home PC which has a mechanical disk. Making an external library of its gui primitives and functions could be an interesting project to be used on small embedded boards.
Since AzPainter v2.1.3, `mlib` sources shipped with AzPainter now licensed under GPLv3 terms, but there is AzPainter theme editor (older `mlib` minimal demo app) which sources licensed under BSD terms:
This way you can have the speed and memory tightness of C++ where it matters, but do the other stuff, like initial data parsing and munging or overall control and sequencing in Python, thus avoiding the general clumsiness and unproductivity of C++
IME, 'C with classes', or C++ but skipping the more esoteric features(OOP overuse, template metaprogramming) yields a fairly easy to understand language for most C folks. I'm a C guy and am most comfortable with it, but I like having the advanced data structures out of the box. Granted, most C codebases of any reasonable complexity have their own support for relevant data structures, but they're a lot cleaner to use via the STL.
Python is a great tool for a certain class of jobs. I saw a company doing systems programming in Python and originally thought it was a great idea, and then I learned of the hidden(or not) dangers of Python.
In August this year I started a new project. In C. Why? Because I'm quite familiar with it and because it allowed me to focus on the problem rather than on the language that I was writing in (the problem is - for me at least - a very hard one). The codebase has steadily grown, at some point it was 7K lines, then I cleaned it up and refactored it back down to about 5500 right now. At some future date it will be - hopefully - finished and I may use it to power applications and/or services. It's super efficient both memory and speed wise compared to what the competition is doing but that's just a side effect. That side effect does allow for some applications that would otherwise be impossible.
The code takes one file and outputs another, it's a classical 'unix filter' and it relies on a couple of very high performance libraries that others provided, battle tested and ready to be plugged in. As the project matures I come across the occasional wart in the language, something that I know I could have done more elegantly in a language with richer constructs (lists, list comprehensions, that sort of thing).
What surprised me most after working on this project for a couple of months though is how well the language fits the problem, that's something that I did not really think would be the case when I started out with it. But as time passes and things 'find their place' it is indeed just like the author writes, I - still - love coding in C. Even if I know there are better alternatives out there and I should probably get invested in one of them one of these days at the 75% mark or so of my career I'm still very happy that I picked C at the start and never once did I imagine that it would still serve me this well 37 years later.
Most of the other 'hot' languages of the days that went by in the meantime have all been lost, and yet, C is still here, and likely will still be here for years to come. To me it's like an old knife. Not pretty, known to hurt you if you abuse it but still plenty sharp and well capable of doing the job if you treat it well.
I love the (seeming) resurgence of C, Rust, and C++ articles on HN. Not sure if it's robotics, IoT, webdevs longing for simpler and stricter frameworks or what. But I love it.
For me, it’s embedded. So I use C, mainly because as a professional company releasing a real world product my only other choice is C++ and I haven’t found a “need” for it yet.
Experience shows that almost every time someone writes a project like this in C, applying modern fuzzing tools (e.g. AFL) will uncover bugs that let a maliciously crafted input file hijack execution and act with the privileges of whatever's running the code. It is a lot of work to remove all such bugs (and not reintroduce any) and you never know when you're done. That is a very severe problem with C that is having devastating effects on society.
It is therefore irresponsible to use C for a new project like this, unless you know your code will never be exposed to malicious input. (Note that if you share the code with anyone else, it's hard to have such confidence.)
Passing things by reference is safe. Any other use is mostly just pointless; it what reference types are for. If you are doing something else with them, it easy to stop.
Passing parameters by reference is certainly not always safe. You can, for example, pass a reference to a deref'ed unique_ptr into a function call, while the function call does something that clears the unique_ptr.
If you want to avoid objects containing references (or raw pointers), then you have to avoid using lots of standard library stuff --- `string_view`, C++20 `span`, or even STL iterators.
Isn't Rust right now (1) still a niche language (2) terribly slow to compile (3) a bad fit in the first place since I don't actually know the language and (4) still immature?
As for libraries, the most important ones that I rely on deal with the loading of .wav files, writing of midi files, decoding of mp3s and fftw ( http://www.fftw.org/ ).
1) Define "niche language". Lots of people are using Rust in production. Rust isn't in the top 10 most popular languages, but it is in the top 100, and rising.
2) Compilation speed is a problem for some projects, but not for small projects like yours.
3) Understandable, but writing exploitable code because you don't know a suitable alternative language very well is not a good long-term situation. Maybe you should do something about that.
4) Define "immature". It's not one of the world's most mature languages, but it is definitely mature enough to write production code for many contexts. In many ways C is the language that needs to grow up.
There are Rust libraries for all those things (including Rust bindings for FFTW itself), though I can't speak to their quality. I can confidently say that using Rust libraries in a new Rust project is a lot easier than using C libraries in a new C project, especially if you're not on Linux.
> Lots of people are using Rust in production. Rust isn't in the top 10 most popular languages, but it is in the top 100, and rising.
I've yet to come across anybody outside of the HN crowd that knows about or uses Rust. Not a single company that I looked at in the last year used Rust, that's 41 of them and I asked every one of them which programming languages are in use.
> Compilation speed is a problem for some projects, but not for small projects like yours
ECT cycle (edit, compile, test) is a pretty big factor during early stage development. Running a test in a second versus running a test in ten seconds would ruin my day. I'm not sure what the current speed of compilation is for Rust but the only time that I looked at it (very early on) it was so slow as to be unusable from my perspective. I'd hope that has substantially improved. Think of what I'm doing as explorative programming, I'm both trying to understand the problem and trying to solve it at once.
> Understandable, but writing exploitable code because you don't know a suitable alternative language very well is not a good long-term situation.
That may be true. At the same time, Rust may not be a good long term solution either, languages come and languages go, and before I invest a year or so into a new eco-system I'd like to see it has staying power. It is interesting that you recommended Rust and not say Java which has pretty good performance, is memory safe and is in production for well over a decade, for this particular use case I would choose that over Rust.
> Define "immature". It's not one of the world's most mature languages, but it is definitely mature enough to write production code for many contexts.
There isn't a month or we see an announcement of the next point release of Rust on HN. I don't have the luxury of tracking a moving target next to working a full time job and having a family, then doing this hobby project besides. If Rust wants to see mainstream adoption for stuff like this because you seriously believe that writing a new project in C is irresponsible (which smacks of FUD, and is one of the reasons why I think Rust is one of the more toxic language communities on the internet, I have never seen a Java proponent use that kind of language) then I hope you agree that getting Rust to some kind of long-term stable form in the very near future is a must. This whole 'the sky is falling' tactic is rather off-putting, at least, it is to me. The same happened to perl, which was supposed to be - and still is according to some - the best thing since sliced bread.
> There are Rust libraries for all those things (including Rust bindings for FFTW itself), though I can't speak to their quality.
That's good to hear.
> I can confidently say that using Rust libraries in a new Rust project is a lot easier than using C libraries in a new C project
For you, as a Rust user, yes. But you are forgetting that for me that is not at all easier, and that using C libraries in a new project is a lot easier than using Rust libraries for me because I happen to know how that works.
Vantage point is a bit of an issue here, I take it that you are fluent in both Rust and C and that you have decided to hitch your wagon to Rust. I'm fine with that and no doubt in the long run you will be proven right but to me it smacks of 'you should do as I do' rather than that it is the best for me.
Learning a new language just for some project is a very high degree of friction to add, it would slow me down tremendously and it might lead to the project being abandoned rather than progressing at a quite acceptable speed given my constraints in time.
> especially if you're not on Linux.
I wouldn't dream of developing software on anything else, but I totally respect other people's choices in their platforms.
> I've yet to come across anybody outside of the HN crowd that knows about or uses Rust.
Interesting. High school students and university students that my kids know are using Rust (in New Zealand, not some high-tech mecca).
> Think of what I'm doing as explorative programming, I'm both trying to understand the problem and trying to solve it at once.
OK. I might recommend Julia then.
> It is interesting that you recommended Rust and not say Java which has pretty good performance, is memory safe and is in production for well over a decade, for this particular use case I would choose that over Rust.
Sure, Java sounds like a fine choice. I didn't know much about your requirements when I suggested Rust. People often choose C because of strict performance or deployment constraints, which Java often can't meet, which makes Rust often a safer recommendation.
> There isn't a month or we see an announcement of the next point release of Rust on HN.
That's because they release every six weeks. That doesn't mean the language is unstable or you will keep having to make changes to your code. It means there's a steady flow of incremental improvements that preserve backwards compatibility.
> you seriously believe that writing a new project in C is irresponsible (which smacks of FUD, and is one of the reasons why I think Rust is one of the more toxic language communities on the internet, I have never seen a Java proponent use that kind of language)
In hindsight I shouldn't have mentioned Rust at all. I do sincerely believe that propagating C code is irresponsible and the software industry needs to recognize this. It sounds harsh, but I think that's partly because software developers have historically taken too lightly the consequences of their choices (even for hobby projects).
People like me who are zealous about the industry moving away from unsafe code tend to be big Rust fans, because Rust finally makes that possible for the systems/embedded domain where C/C++ were for such a long time the only viable option. I guess we have skewed the Rust community to some extent. Mea culpa.
> it smacks of 'you should do as I do' rather than that it is the best for me.
When we write code and distribute it, we have an effect on the world, and then I think it behoves us to consider more than just "what is the best for me".
For hobby projects where the code is never distributed, these issues are mostly moot ... though it's amazing how often such projects escape sooner or later.
> I do sincerely believe that propagating C code is irresponsible and the software industry needs to recognize this.
I believe that you are sincere in this. At the same time I ask you to recognize that the way you - and many other proponents of 'safe' (safe between quotes because computer programming will never be safe) languages approach this is combative and therefore ultimately un-productive.
The old proverb says that you will catch more flies with honey then with vinegar and headbutting with people and calling them irresponsible is not going to get you where you want to be. It will have the exact opposite effect, people will dig in and their resolve will strengthen rather than weaken. This is because people tend to be invested in their tools and their creations. Going for a full on frontal confrontation about this is counter productive, that's just human psychology, which is in cases like these as important - if not more important - than having a technological edge or being right.
Zealotry is the last thing you want to take with you in your toolbox if you want to make change.
That's a good point, but it assumes the only audience for this conversation is developers already emotionally attached to C, which I don't think is true.
GC is a problem for OS/embedded programming. And before Rust, safety required GC (or an unpalatably restrictive programming model like MISRA). Even if Symbolics or the C# Windows Vista work had been more successful, Rust would still be needed.
> Even if I know there are better alternatives out there
I could have chosen Java, Python, Go or C++ instead. Instead I chose the tool I'm most familiar with because that will get me focused on the problem rather than on the tool. It's a disadvantage of getting older: there is less time left to waste.
> To me it seems you know it can be dangerous, but like it too much not to use it.
It is mostly a familiarity thing, not a like or dislike.
I think we also choose our favorite programming languages based on our psychology, not only the technical features.
The ones who like C and C++ are like Toretto from the Fast and Furious franchise choosing a classic muscle car for a race. They are super fast, give you much more options, but because of that, they are not the safest.
A muscle car requires more skill to drive and win than a modern fast-shiny-and-safe car. But because you know they are more dangerous to drive, is likely that you will try to improve your skills to make it less likely to have an accident with it.
Thats one of the reasons why im still skeptic to jump into the Rust bandwagon, once you turn into a good muscle car driver, its hard to choose for safer but more limiting options, but you totally get it why someone without experience in fasters cars would choose for the safer version.
I’ve been messing with yasm lately and I think I might like that more. I’ve written a lot of C (including a small browser) and definitely enjoy it (especially parsing, which is probably not great.) The more I’ve written and read though the more I’ve realized how much it really does abstract and that kind of sucks the joy out of it for me.
For actually getting things done there’s go and python of course.
So I have to hope for the best when some piece of hardware, coded in C, gets plugged into the network.
Morris Worm is 30+ year old, and yet best we can do is having platforms like CHERI, Solaris SPARC ADI, ARM MTE, which also happen to be quite specific.
C has a bad rep, and deservedly so in the context of anything related to security. We have not been able to sanitize the language because the ability to surprise the writer of some bit of code is almost a core concept in the language. This sucks. But at the same time the way newer languages are held up as the manna from heaven is rather tiresome as well. By the time they will be deployed in the quantities that C code is today there will be exploits in those languages because it is mostly the programmers that are fallible. Now, some languages - again, C - make it easier to shoot yourself in the foot than others. But I've seen plenty of code in Java that was exploitable even though memory safety in Java is arguably at a higher level than say Rust.
So you'll be hoping for the best for some time to come, no matter what the stuff you install was written in.
It is possible to foreclose on some kinds of error entirely, and if you compare like-for-like, those benefits become obvious. If, on the other hand, you take the productivity gains of a higher-level language as a reason to write more ambitious programs, well, new functionality can find new problems.
But every language that can reach the hardware will have the ability to wreck things in spectacular and hard to predict ways. Every new language ever was touted as the one that would finally solve all our problems. For Java and COBOL the historical record borders on the comical. I have no doubt that the same will go for every other language that we just haven't found the warts in yet. Two steps forward, one step back, that seems to be the way of the world in the programming kingdom.
Very nice, but I'd nitpick memory corruption and expand it to hardware errors, or add that category.
Sending digital signals over analog medium (read: always) can fail, and may do so regularly depending on hardware itself and environment conditions.
Simple examples: Digital 3.3V signals sent over 10m, high-capacity lines (lose a bit here and then), or an overclocked CPU undervolting just at the right time, etc...
EDIT: I'd also argue that UB is a type of logical_error. Not the fact that UB exists within the language, but in that the logic failed to account for the scenario?
Except that unless we are talking about hardware design languages, no programming language accounts for hardware errors as such.
UB is its own kind of error, specially since ISO C documents over 200 use cases, and unless you are using static analysers, most likely won't be able to find out that are failing into such scenarios as no human is able to know all of them by heart, whereas logical errors are relatively easy to track down, even by code review.
Nearly every piece of networked hardware has critical software running written in C, and the consequences are not nearly as disastrous as reading HN would make us believe.
A possible outcome is that you would trade pointer bugs etc. for "Java bugs" if Java embedded were used everywhere. Embedding a complete runtime increases the attack surface alot.
Many of those Java exploits are on C and C++ written code layer, yet another reason to get rid of them in security critical code.
According to Microsoft Security Research Center and Google's driven Linux Kernel Self Protection project, the industry losses due to memory corruptions in C written software goes up to billions of dollars per year.
Then, unless we are speaking about a non-conformant ISO C implementation for bare metal deployments, it provides the initialization before main() starts, floating point emulation, handling of signals on non-UNIX OSes, VLAs.
How Apple, Google, Microsoft, Sony, ARM are now steering the industry regarding their OS SDKs has already proven who is on the right side.
I have been at this since the BBS days, I don't care about Internet brownie points.
The only thing left is actually having some liabily in place for business damages caused by exploits, I am fairly confident that it will eventually happen, even if it takes a couple of more years or decades to arrive there.
Claiming that a little process startup code (that isn't really part of a particular language, but is much more part of the OS ABI) was easier to exploit than an entire JRE is just dishonest.
I would never think of "floating point emulation, handling of signals on non-UNIX OSes, VLAs" as anything resembling a "runtime". These are mostly irrelevant anyway, but apart from that they are just little library nuggets or a few assembly instructions that get inserted as part of the regular compilation.
By "runtime", I believe most people mean a runtime system (like the JRE), and that is an entirely different world. It runs your "compiled" byte code because that can't run on its own.
I care about computer science definitions, not what most people think.
Dishonest is selling C to write any kind of quality software, specially anything conected to the Internet, unless one's metrics about quality are very low.
Undefined behaviour is a tool to generate faster code, by shifting some burden of correctness to the programmer. For example, accessing past the array size is undefined behaviour. This lets the compiler skip the bounds check, an expensive check+branch on each array access. The trade-off is that now the programmer has to ensure correctness manually (or use an alternative interface that wraps a bounds check) else bad (TM) things will happen.
Despite this, tools will often not warn of undefined behavior or these premature optimizations. Quite the opposite, the compilers are almost user hostile for trying to ensure safety and correctness. I cannot see into the minds of the GCC and LLVM teams responsible for the C frontends, but to the extent that you'd think the compiler should do anything here, they seem to think it's largely not their job to improve safety or correctness. That's the job of static analysis tools (which can cost a lot of money), runtime analysis tools (which can cost a lot of time in writing tests that exercise all code paths), and fuzzers (yet more time).
Punting all of this extremely important reasoning to "the programmer" is ridiculous when it's been proven over and over again that even incredibly smart C programmers get this wrong.
Compilers could certainly do a better job when performing optimizations that “abuse” undefined behavior, but in general this isn’t a trivial problem. The way optimization passes are set up can often make it difficult to generate warnings that have a low false-positive rate.
Do you think it might be a problem that undefined behavior optimizations and/or pattern matches are so prolific in C that reducing the "false-positive rate" of ... actually bad undefined behavior (?) would be the hard thing?
Why not disallow undefined behavior and instead have the compiler emit: "Hey, this branch is never taken, if you want us not to compile it remove the dead code". Seems like that's exactly the same sort of putting the onus on the programmer that andrepd mentioned, but without the side effect of millions of device drivers are zero day remote code execution vulnerabilities waiting to happen?
The array access example shows why this isn't generally possible -- the programmer hasn't written any bounds-checking code or assertions (they just wrote x[y]). Should the compiler emit bounds-checks? If yes, your code is now slower but there isn't an easy way to disable this "feature" (if x is heap-allocated or passed as a pointers, how will the compiler know what kind of bounds check is sufficient?). If no, you have undefined behaviour and crashes.
Even Rust made a mistake with restricting undefined behaviour -- they accidentally allowed creating references to fields in packed structures (resulting in possibly unaligned references which is undefined behaviour in Rust) in safe code. These days you get a warning telling you to use unsafe or copy the value, but they have yet to make it an error. For comparison, C also gives you a warning for the same problem (or an error with -Werror).
I also want to point out that modern compilers have different sanitisers (-fsanitize=...) which cause your program to abort() on memory unsafety or other violations. It's effectively a better valgrind which is compiled into your program.
I would argue that most of the time, yes, the compiler should emit bounds checks and only in performance sensitive code should they be removed (or where the compiler can prove the bounds check holds for a range).
I don't know how anyone who has studied the history of vulnerabilities and security issues and code would think this is a hard tradeoff:
> If yes, your code is now slower but there isn't an easy way to disable this "feature" (if x is heap-allocated or passed as a pointers, how will the compiler know what kind of bounds check is sufficient?).
> If no, you have undefined behaviour and crashes.
The latter choice has resulted in how many millions or billions in losses, perhaps even lives lost?
The problem is that if you want to permit users to do things like dereference pointers and do pointer arithmetic (a requirement for a C-like language) there is no way for the compiler to ensure (even at runtime, because the pointer might not be to memory managed by the language) that the pointer dereference is valid.
Okay, maybe you can add a small overhead to array accesses (which is what Rust did), but you can't stop undefined behaviour entirely. Rust has plenty of undefined behaviour (most can only be triggered through unsafe -- which is the whole argument behind the language -- but as I mentioned there are some cases where even they made a mistake and made an operation safe when it should've have been).
The benefit of "unsafe" blocks is that the surface area to audit is dramatically reduced, so that yes, if you want high performance pointer mangling and arithmetic, you can do it, but your code will require more attention.
In C/C++, undefined behavior and unsafe operations abound and it is nigh impossible to isolate what is "important" to review from a security standpoint.
> Why not disallow undefined behavior and instead have the compiler emit ...
You can't "disallow" all undefined behavior in C without fundamentally changing the language to something different (Which would also likely make it useless for it's intended use-case). Some of the details related to undefined behavior are simply not possible for the compiler to detect without extra information, hence why they are undefined. `Rust` has the same thing, you can invoke undefined behavior anywhere in the program through use of `unsafe` anywhere else. And Ex. How is the compiler supposed to determine if a particular function is ever called with a `NULL` parameter? How does it know if a particular signed addition may overflow? How does it know if two parameters to a function alias in an invalid way? How does it know if a particular pointer lacks a `const` and is actually backed by read-only memory? And most importantly, how does it determine all of this at compile time?
With that not all undefined behavior-based optimizations are unexpected or warrant a warning or error (In-fact, I would say at the very least the majority of them don't). Undefined behavior is much more complicated then just dead-code removal. Even then though, you don't always want dead-code removal warned about - Imagine getting a warning about every inline function or macro that results in some dead-code.
To be clear, I'm not saying there aren't obvious problems that have come from this approach, and I think some of the choices for undefined-behavior are clear mistakes now even though they made some sense in the 80s or 90s (And some of those can usually be turned off, thankfully). And I think there are also some additions that could be made to the language that could solve a lot of the existing problems (Like non-null pointers, or fat pointers, though I'm not exactly holding my breath...), but the existence of undefined-behavior is not in-and-of-itself the actual problem with the language, and "disallowing" it outright is simply not possible. I would actually wager undefined-behavior exists in some form in a lot more languages than you'd expect, just less documented. Rust certainly still has it, despite it's numerous safety-focused features. And even if you add things like fat pointers to C that would clearly reduce possible out-of-bounds mistakes, the language would still need to provide some way to create one from a size and a pointer, and that still allows for undefined-behavior cases to happen - my point being, while adding such a thing is still clearly better, it does not actually remove the undefined-behavior possibility.
It's not very expensive because the branch will never be taken except when it results in an out of bounds access. When that out of bound access happens you care more about the security benefits than performance.
I'm curious if this still stands in general today, given today's advanced branch predictors and the fact that the CPU tends to be memory-bandwidth-bound, thus you have more "free computation" while waiting for memory (what GPU programmers call compute to memory ratio).
C is fantastic for small and very optimized components. If you want it portable, from microcontrolers to GPUs, it is the best choice too.
But if you need rapid prototyping, large scale projects within an organization or some libraries and tools to make the perfect, animated, templated and themed user interface you should think twice before choosing C. Java and .Net are freaking good at database integration and memory allocation/dealocation. Erlang is fantastic for multitasking and multithreading. Ruby on Rails or Python on Django are much better to prototype a website nannying a database.
You already know the saying "if your only tool is an hammer..."
Came to make a similar point. There's lot of love for C and belief it's some superior language to modern ones. The reality is that C hasn't aged well as hardware has evolved. It's not that good as parallel programming, which is more prevalent considering most CPUs are multi-core. The ACM did a great write-up on this, https://queue.acm.org/detail.cfm?id=3212479
The bottom line is to echo your first statement, C is a tool, good for some tasks, not so good for others. Choose accordingly.
That's one of the good points about using C today. Unlike, say, Fortran (or any other language with a high-level semantics) C may be hard for a compiler to optimize for a modern CPU.
If it had native support for JSON and supported GO like co-routines then a lot of its utility would be applicable to other scenarios that use fancy languages. This is what I always find surprising, C’ syntax is robust so add these missing gaps to the current language and weall win.
Isn't that basically Go's whole thing? It has syntax that is really close to C, Go style co-routines (obviously), and I believe it has good JSON support. You do have to use a GC and you lose some of the really low level bits but I would wager most C programs don't need to have no GC and be able to do the really low level stuff. The ones that do probably don't need the goroutines and JSON and stuff.
I have an embedded project with an RTOS for things you would do with coroutines, CBOR and JSON handling. I would not appreciate a garbage collector when it came time to debug a dumb pointer or overflow error.
Doing it in C obviously. I can’t really consider Go embedded because I’m maxing the chip out now in C without including a Go VM or whatever you call it.
>C’ syntax is robust so add these missing gaps to the current language and weall win.
If every feature everyone wanted was added to C, it would just turn into a bloated mess like C++, or like PHP, with tons of bad decisions baked into the language that can't really be avoided. Features like this belong in libraries - the stdlib should remain as minimalistic as possible.
If everything everyone wanted was added to C, it would be C++. :-)
The C++ evolution is essentially an iterative cycle where people hackily create new language features with insane preprocessor and template hackery, and then the Standards Committee comes in and takes the most popular hacks and tries to turn them into real language features. And then people create new hacks on top of that.
C says, you want object oriented polymorphism, you've got structs and function pointers, what more do you need?
I participated in the coding of several arcade games in Z80 assembler in the late 80s. Not "a" large project but a collection of small ones.
These programs, by the time I came aboard and started sharing code with those to make the games I was involved in, had also "succumbed" to this force.
The shared bedrock of all these games was code that used certain Z80 registers as pointers to take advantage of instructions that read or wrote fixed offsets from those pointers. The first byte of the record pointed at by the pointer was the type and the next 4 bytes were the position. The rest of the record depended on the type.
That was us doing the best we could with 8-bit processors when our competition was using 68000.
Many C projects indeed have polymorphism (I believe io libraries do exactly that to implement the idea that "everything is a file"), but this is rather different from object-oriented polymorphism.
This is of course implicit in the mindsets -- C++ is continually trying to support new idioms for the programmer to express themselves, C would prefer you stick as close as possible to a smaller set of idioms. Just because you can technically make use of a design pattern in C, doesn't mean the language (or the community) will ever encourage that.
And is now obsolete, for the most part. Unless you want to do something on hardware that only has C support, there are generally better options. The language has not evolved. There are now several good alternatives in this space.
Edit: Rather than responding to all the comments individually: There's obviously a lot of code already written in C. That doesn't mean it makes sense to write new code in C. Sometimes C is your only option, which I already said in my original comment.
None of that means there is any reason to start a new project in C if it's going to run on Windows or Linux, and it's certainly not the case that you have to write your program in C if you want to call it from another language. Sorry if this disappoints anyone, but it's true. For the traditional uses of C, such as writing a text editor or a library for numerical computing that you're going to call from another language, there's no longer a sensible argument for using C.
Linux (like the BSDs) being C is an organizational phenomenon. There is no plausible technical reason for it not to be, or include parts in, C++. History is path-dependent; how we got here matters more than what is around today.
There is really no excuse for systemd being C. It is really a drag on its development.
UNIX and C are symbiotic, to get rid of C we need to get rid of both.
On the OSes that aren't just plain UNIX clones, C has a much less relevant role, and in some of them it is even being phased out, even if it might take a couple of decades yet.
It used the Pascal calling convention, however Lisa and Mac OS were implemented in a mix of Pascal and Assembly.
Pascal UCSD also used a mix of interpreter and later AOT compiler.
Most mainframes don't use C as their original implementation language, and the oldest still being sold (Unisys ClearPath MCP) was designed about 10 years before C was born, in an Algol derived language, that uses instrics instead of Assembly.
If one broadens their horizons beyond UNIX, and looks into the history of computing there are plenty of interesting languages and OS architectures to dive into.
The Internet is full of digitalized version of papers and computer manuals from those days.
At the time everyone was sure that Pascal was The Future of both OS and app development.
What happened instead was that everybody had their own Pascal dialect with the extensions it needed to be actually useful. Meanwhile, C had them all, already, and was about the same everywhere. Soon after, C++ came along, and there was again only one (although Microsoft delivered workable template support very late).
Lesson in Network Effect there. Also, shooting for usefulness out the gate.
Meanwhile, the Pascal crowd went off with Modula-2, Modula-3, Oberon, Apollo Pascal, UCSD Pascal, Object Pascal, Delphi, and countless others, none compelling. Apparently Delphi (still) has economic importance. The rest, not so much.
Except the entire world basically runs on top of C, and any library that wants to be usable from different languages is still written in it. Which is a pretty weird definition of obsolete. Don't use it if you don't want to for whatever reason, but spreading FUD to protect your bubble doesn't help anyone. C is the most successful programming language ever designed, and a fine tool in the right hands. It's not perfect, nothing is, anyone telling you otherwise is lying.
> any library that wants to be usable from different languages is still written in it.
This part isn't quite true. All that's needed is to be able to define an exported function with a specific name and calling convention. Libraries can be written in any language that supports these features.
That's just not true, because C++ won't even allow many of the techniques that make C the awesome language it is. It's different, and that's about as far as it goes.
Add name mangling, ABI issues and crazy compile times to that.
It was potentially safer until they added Exceptions, which tipped the scales so far the other way I can't believe we're still having this discussion.
There's Itanium ABI, you can use C type interfaces when needed and suppress name mangling. How do exceptions make it unsafe? They're part of idiomatic C++ and solve a problem without anything unsafe about them. And what are the many of the "awesome C techniques" that you can't do in C++?
But you won't when writing C++ unless you absolutely have to, because it's a major pita to encapsulate everything.
Entire books [0] have been written about exception safety in C++. Getting it right in combination with RAII, (copy)-constructors and assignment operators is very tricky. Which touches the main issue with C++ from my experience, mixing foot guns with higher level convenience.
Embedded linked lists [1] is a good example.
Seriously, live and let live. I'm fine with you enjoying and preferring C++, go for it. But consider taking a good look in the mirror and asking yourself why you feel the need to spread FUD about C to feel good about your choice.
I can honestly say that C++ exceptions have never, in decades, cost me a single hour's lost sleep. If you have any trouble with them, I promise that you are Doing It Wrong. You can tell you are Doing It Wrong if it is not super-easy and super-clean.
The key is to make sure all your cleanup code is in destructors where it will be exercised frequently as part of the normal operation of your program. In the event an exception is thrown, a bunch of destructors run, as usual, and your program ends up in a known clean state, with no extra effort.
There is no good alternative for doing some low level thing where you must have fine control over IO or execution sequence.
Rust is the best candidate, but it operates on a completely different level. (That said, I don't think one ever need that much control on a PC or a server. Not even on kernel programing.)
The language hasn't evolved for the same reason a transistor hasn't evolved: because it didn't need to. Adding for example string manipulation, or graphics, etc. would make it a fantastic language, but it would lose being that minimalist tool which makes it often the best choice when writing low level stuff. To me, Crystal, Nim, Rust, Go etc. already fulfill that need.
About the electronics comparison, I can build a lot of complex stuff more fast and more easily using opamps or logic gates compared to discrete transistors, but if I need to do a simple basic task such as invert a logic signal or amplify an analog one I'm using a single transistor any day because it doesn't just spare all that wasted silicon in chips but also in many cases it will be more fast, more cheap and more quiet noise-wise.
Wrong. Transistors have evolved in dozens of directions. The old transistors are still used, but decreasingly so. It is likely an op amp does better what your transistor and a bunch of other stuff did, and equally as cheaply. The op amp might have bipolar transistors in, but just as likely not.
C has not evolved because C++ has, and because the computers got a million times faster, so even Python is usually good enough. If you need something that does what C does, but better, C++ is right there waiting. Change your makefile, or rename some files, and suddenly you have a C++ program, that is then easy to improve as much as you like.
C has barely changed since 1989. It already had void and function prototypes copied from C++, back then. Only atomics and restrict make any real difference.
I meant functionally, ie what a single part (bjt, jfet, mosfet etc) can do. of course there are better materials and ways to produce more advanced parts.
It's the only language your chip vendor will give you, but there is very little reason ever to use their compiler, anymore. And they have no say in what you use to generate your machine code.
Anyone that is open mindend can get Pascal, Basic, Java, Oberon, C++ and Ada compilers, although I admit that the Ada option is only available for deep pockets.
When you hang yourself with C, at least you're hanging yourself with something real. It's more embarrassing to be done in by a misunderstood abstraction in a high-level language than a concrete memory overwrite in C.
You can hang yourself in C by having code that would dereference a NULL pointer that never actually would be executed, yet the compiler “optimises” your code based on it. Is that “real”?
I think the hanging yourself in c comes from the syntactic complexity, not its high-levelness.
Several mistakes in c:
Using string interpolation preprocessor macros makes it so that you have to 1) learn a whole second language to read other people's code (most portable code requires using #defines) and 2) it's very difficult to trace through code where you don't know where some values are coming from.
Header/code segregation seems like a huge mistake. It's far more effective to have a single file with well-defined exports. Even the #include system is broken, because it's not explicit or obvious which directories one must go to to find header files (they might not be local to your project)
Sigil order. The fact that there is a guide saying you should "spiral out" to understand how to read a type should be evidence enough that this is a huge mistake.
I get it that there are professional c coders that will say that's part of knowing the language. I'm not a professional c coders, I program in another language and very rarely need to dip into something lower, say for performance or mutability. Thus the c code that I should write is dangerous, I need safety, and simplicity. Luckily, I found another language that hits those spots.
syntactic complexity is just the surface, just wait until you compile and run the program and watch buffer overflow, strict aliasing, signed overflows, undefined behavior, and other optimizations and "you have direct access to the memory"-cargoculting silently expose all kinds of vulnerabilities in your program.
It's still a great language and still the industry standard for embedded. If you can't figure out C, an incredibly small and simple language, you probably shouldn't be programming.
We used C99 in some of my classes at college, despite attending University from 2010~2017. I liked C a lot - working that 'close' to the hardware and the OS really gives you an appreciation for how things work. These days I've moved on to things like Ruby, Python, and Go...but I'd like to give it another try with C18 to see what modern C looks like.
This is timely! I never really learned C deeply (other than from a class in high school where pointers were considered 'too advanced'...so we didn't really get too far).
I decided to work on this year's Advent of Code in C and it's been a blast so far. I finally "get" pointers, malloc/free, etc. (and I have a newfound appreciation for Ruby things like CSV.each).
Since most of my coding work is at a much much higher level (web backend & frontend stuff), I probably won't spend too much time in C in the regular course of business, but it's massively helped me better have an intuitive understanding of previously vague things like "allocate on the stack vs. heap" and "pass by reference vs. value" -- I knew what those words all meant but didn't really have a feel for what they actually did. C helps with that.
If you want to write a library that can be used from many programming languages in many operating systems, exotic processors and application plugins, use C.
If you want to write a new programming language that does the same from the get-go, make C as one of the compilation targets.
Got as far as playing around with a web site that used Rustler NIFs on a Phoenix website. Why? It was just fun to figure out how to get something from the web frontend through Elixir and into some Rust numerical code.
I don't think it had much practical use and knowing me, would be ridiculous to go back and touch it again (likely). I've been thinking about doing Rust to WASM but I'm still not sure if it's just throwing a bunch of complexity at something just because. It holds my interest a lot more than just doing everything in JS, though.
C was also more enjoyable to me than other languages. Something about compiled languages just feels really nice. But, it's also great to have a REPL available right there. Like with Elixir.
I used to enjoy C. If you enjoy C for these reasons, you may enjoy Go and the benefits of living in this millenium. It feels very minimal with a lot of control. Similar heritage. Definitely feels like C a lot of the time. And you get to use the & operator if that's your thing. It's got cleverness to infer types if you want, but you can specify types and a lot of the cleverness is at compile-time: still very light-weight to run. And it's got a better standard library which makes it a lot more fun to throw something together quickly.
When it comes to simplicity, Go is definitely quite similar to C in many respects (and not all of them good). After I started working with Rust for a little bit, I think it better matches the feeling I get with C when it comes to being confident what your code will do when compiled. To be fair, there is still a fair amount of magic you have to get used to.
But the main issues that I've had with Go is when the Go runtime is causing your code to very much not act like a C program. To be fair, I work on container runtimes and similarly "lower level" tooling so this is probably not an experience that everyone else has.
I haven’t used it much, but I really dislike Go despite liking C. It adds new things but just barely, to the point that it makes me want to not want the watered-down features it has.
I DO TOO! Warts and all, the simplicity is really refreshing, especially in an age of:
"Make sure you have homebrew installed and then `curl http://goodluckbro.io/doomed | sh` then go read the Medium blog post to write your first hello world"
And don't get me wrong, I love and use whatever-shiny-lang-v2 too, I just really like the simplicity of C (and yes I acknowledge that comes with the ambiguity and undefined behavior too).
For those who appreciate radical simplicity I can recommend the Oberon programming language. As opposed to C it is a real high-level language whose language specification contains no reference to any computing mechanism:
I'm finishing up an embedded side project over the holidays. It's C, FreeRTOS and the STM USB host stack. I spend most of my time with the likes of React JS, Swift and Kotlin so it does give me a good, honest, fresh feeling. Also the feeling that my code could rise up and smite me at any moment! Great read, thanks for posting!
I wish C had gone back and done a better implementation of strings that wasn't null terminated. Or at least an implementation of plain buffers with a richer libc for manipulating them.
I know you can find bolt ons, but they aren't terribly useful since no 3rd party libraries use them. You have to resort to stuff like antirez's sds.
The choice is natural for C, due to the fact that it is the only way to implement the string simply as an array of characters. (Doing anything else would go against C's view of the world.)
https://github.com/antirez/sds is heaps better than just using raw strings. Any language with a fat pointer variant out of the box is going to do it better than C as well.
That's not to say that the rest of C is bad, but there's nothing to be gained from defending C strings; they're bad and you should probably use an alternative and that alternative should probably have a length and a buffer, even better if you can find this out of the box in your language that otherwise works a lot like C.
C's "inherent inner workings" appear to rely on the fact that every char array has to be null-terminated.
That then implies at minimum a sanitation of user- and data-input of char array, and maybe an absolute maximum overrun char count protection at execution, but once your default is to keep track of every string length and not the opposite of keeping track of specific strings, I'd kinda agree with above that it's not really "C"... more like your own implementation of a(n overrun-safe) string library?
Inherent to the concept of the char array is that a size value would be redundant, for it is the position of the null-terminator that already contains/is this.
The analogy for the string concept of a char array's missing null-terminator is actually a wrong size value.
I thought the OP was a little bit self-undermining, because as the author points out, C feels "close to the metal" but at this point it's actually virtual metal.
But I think "getting close to the lower levels" is still great. The only time I write C-like code is for an Arduino. Turns out that even if your electronics is rusty, you can do some very useful things in the real world by automating a simple motor and a few LEDs. And whenever you have to interface with individually numbered output pins, it feels pretty low level.
tldr: if you want to get close to the metal, it's really useful to start by switching to hardware that facilitates this.
Even assembly goes through a number of layers before it’s run. With microcode, virtual memory, and privilege rings there’s a number of things between the code and what it’s running on.
Yeah, that's why I used expressions like "closer to the lower levels" — you can descend in orders of abstraction/layers of reality, but it's more asymptotic or directional than anything else.
It's not like playing with an Arduino is getting me closer to understanding the underlying chemistry and physics. Which are also interesting to learn about...
But personally I think it's for the best that reality is inexhaustibly complex (from the perspective of human individuals).
> C feels "close to the metal" but at this point it's actually virtual metal.
Since you mention the Arduino, when you start out with it, you're most likely turning on digital pin e.g. D7 with digitalWrite(7, true), it's easy to understand and quick to implement.
But spend enough time on the Arduino (or C or Assembly or embedded in general) and you'll soon realize that all that function call is doing is basically the same as doing PORTD |= 0x80. That's a bit tougher to understand, but even quicker to implement, and once you've understood how that all works together, a lot of everything about computers and digital electronics becomes so much easier to understand.
431 comments
[ 4.4 ms ] story [ 326 ms ] threadNope, plenty of alternatives have existed throughout the history of computing
https://en.m.wikipedia.org/wiki/System_programming_language
> C as the Ur-language
Not really, ESPOL came up in 1961, while C only became a thing in 1969.
"Physicality" was setting CPU register bits directly via the I/O panel of my UYK-7 back in the day.
Happy not to be cruising at that altitude these days (to use an equally inappropriate metaphor).
The article clearly states this is a subjective statement and isn't meant to be taken literally.
https://youtu.be/XHosLhPEN3k
However, when I'm carefully using C++, I don't have to fiddle around with memory management and still get fast performance and better type checking.
Yesterday I wrote a Python script to generate C code test cases. String manipulation in Python is very easy.
The right tool for the right job, I suppose.
For example: I understand why the standard was written the way it was but syntactic closures in C++ are way worse than GNU C.
[1] https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html
Although I’m not sure I agree about the “ergonomic” thing.
I remember GNU GRUB going through and getting rid of all of them.
I'm not familiar enough with ARM - what does ARM Cortex give you here?
I saw a talk about weird machines. The upshot of it is if you can clobber the return address on the stack you can usually create a weird machine that you can then program[1]. Leads me to believe that if you are putting entrusted data and return addresses on the same stack you're in a state of sin security wise.
https://en.wikipedia.org/wiki/Weird_machine
[1] Also saw an exploit on an embedded system where they leveraged that even though the memory was locked you could set the program counter and read/write registers via jtag. Using that they were able in short order to recover the devices security keys and re-flash it with their own code.
Eg, https://news.ycombinator.com/item?id=21635551 with alternating ro+x and rw-x pages holding thunks and fptr+data pairs.
I have a list of the structure members. I generate an assert for each member being a certain value. The python script builds/runs the C code. On failure, I parse the assertion failure, get the actual value, change the C assert string, rebuild-rerun. Continue until the program succeeds.
I've visually verified the decode is correct once. I want to keep the decoder tested if I change the code again so I'm generating complete test coverage.
Note that Python's official type checker (mypy) is pretty good (within the limits of type erasure); I started the project I've been leading up at work this past year in statically-typed Python, and it's been a great experience.
https://docs.python.org/3/library/typing.html#typing.TYPE_CH...
Looks like there is a work in progress though[1] and people in the ticket provided some workarounds[2][3] for now.
[1] https://github.com/python/mypy/issues/731
[2] https://github.com/python/mypy/issues/731#issuecomment-53990...
[3] https://gist.github.com/catb0t/bd82f7815b7e95b5dd3c3ad294f3c...
I do this quite a bit. Basically, anytime there is something very boiler-platy (like HTTP APIs, configuration management, test cases, etc.) I write metadata in JSON that describes what I'm adding, and then at build time a python script will parse the metadata and generate all of the C or C++ which is then compiled. I even have the python generating comments in the generated C/C++!
I used to use Jinja2 for generating the C/C++ files, but now I just use f-strings in the latest version of vanilla Python3.
There is nothing wrong with generating code in a pragmatic way. It is just strange to see all the time people using workarounds for things that a tiny DSL could probably solve better.
[1] https://swagger.io/tools/swagger-ui/
It's just equivalent to a preprocessor.
A small one-off run under your control is an exception. But people learn to fear the thing, and apply that fear everywhere.
> A small one-off run under your control is an exception. But people learn to fear the thing, and apply that fear everywhere.
Why does it usually suck? Writing code that writes code seems like an optimization that would bolster productivity.
One large factor is that when somebody really dislike the result of a code generator, they most commonly cope by editing the generated code by hand. With other metaprogramming techniques people can't do the same, so they fix the issues.
The end result is that code generators are usually a set of mostly-functioning tools that create bad code that is almost, but not exactly impossible to understand but that you will be required to change at some point.
Few years ago it fully rewritten from C++ to C.
[0] https://github.com/Symbian9/azpainter
Can't answer for now, but check next awesome lists:
• AWESOME C — https://github.com/Bfgeshka/awesome-c
• AWESOME C — https://github.com/kozross/awesome-c | mirror — https://notabug.org/koz.ross/awesome-c
• AWESOME C — https://github.com/aleksandar-todorovic/awesome-c
∗ AWESOME C++ — https://github.com/fffaraz/awesome-cpp
Modern C apps with a GUI typically build on GTK but that toolkit has become massive. Modern GTK3 apps are built on top of dbus, pango, atk, cairo. This is not necessarily a bad thing though and libraries like ATK provide accessibility which is important. At the time GTK+ was created the competing toolkit at the time, under Unix, was Motif and it had the nickname "bloatif". Now we've reached a point where GTK3 is much much larger and "bloated" than a 2019 Motif application.
If we include C++ the Qt and WxWidgets toolkits are also massive. FLTK is still a lightweight option though.
Yes, I was also meaning the ui, which in this one is amazingly fast. I tried a few filters and they seem fast too, but being not a gfx expert I have no way to judge. But the ui is incredible; same speed on my home PC which has a mechanical disk. Making an external library of its gui primitives and functions could be an interesting project to be used on small embedded boards.
Think, AzPainter itself already could be used on small embedded boards ;)
For source code of AzPainter's `mlib` toolkit look here:
- https://github.com/Symbian9/azpainter/tree/master/mlib
Since AzPainter v2.1.3, `mlib` sources shipped with AzPainter now licensed under GPLv3 terms, but there is AzPainter theme editor (older `mlib` minimal demo app) which sources licensed under BSD terms:
- http://azsky2.html.xdomain.jp/linux/mthemeeditor.html
Also, there are few other apps based on `mlib` toolkit
- http://azsky2.html.xdomain.jp/linux/azpainterb.html
- http://azsky2.html.xdomain.jp/linux/azcomicv.html
- http://azsky2.html.xdomain.jp/linux/aobook.html
This way you can have the speed and memory tightness of C++ where it matters, but do the other stuff, like initial data parsing and munging or overall control and sequencing in Python, thus avoiding the general clumsiness and unproductivity of C++
Static typing is a superior approach that makes maintainability and refactoring orders of magnitude easier.
Much better type system than C, syntax is very similar to Python, compile times are excellent and speed analogous to C++.
Python is a great tool for a certain class of jobs. I saw a company doing systems programming in Python and originally thought it was a great idea, and then I learned of the hidden(or not) dangers of Python.
Yes, you definitely need to pick the right tool.
The code takes one file and outputs another, it's a classical 'unix filter' and it relies on a couple of very high performance libraries that others provided, battle tested and ready to be plugged in. As the project matures I come across the occasional wart in the language, something that I know I could have done more elegantly in a language with richer constructs (lists, list comprehensions, that sort of thing).
What surprised me most after working on this project for a couple of months though is how well the language fits the problem, that's something that I did not really think would be the case when I started out with it. But as time passes and things 'find their place' it is indeed just like the author writes, I - still - love coding in C. Even if I know there are better alternatives out there and I should probably get invested in one of them one of these days at the 75% mark or so of my career I'm still very happy that I picked C at the start and never once did I imagine that it would still serve me this well 37 years later.
Most of the other 'hot' languages of the days that went by in the meantime have all been lost, and yet, C is still here, and likely will still be here for years to come. To me it's like an old knife. Not pretty, known to hurt you if you abuse it but still plenty sharp and well capable of doing the job if you treat it well.
It is therefore irresponsible to use C for a new project like this, unless you know your code will never be exposed to malicious input. (Note that if you share the code with anyone else, it's hard to have such confidence.)
So, given all this doom that hangs above my head, what should I have used?
The powerful libraries growing up around it make dropping down to the dangerous level mentioned largely unnecessary, without giving up performance.
If you want to avoid objects containing references (or raw pointers), then you have to avoid using lots of standard library stuff --- `string_view`, C++20 `span`, or even STL iterators.
It is very likely that Rust would be a good choice, especially if the libraries you used have Rust equivalents.
As for libraries, the most important ones that I rely on deal with the loading of .wav files, writing of midi files, decoding of mp3s and fftw ( http://www.fftw.org/ ).
There are Rust libraries for all those things (including Rust bindings for FFTW itself), though I can't speak to their quality. I can confidently say that using Rust libraries in a new Rust project is a lot easier than using C libraries in a new C project, especially if you're not on Linux.
I've yet to come across anybody outside of the HN crowd that knows about or uses Rust. Not a single company that I looked at in the last year used Rust, that's 41 of them and I asked every one of them which programming languages are in use.
> Compilation speed is a problem for some projects, but not for small projects like yours
ECT cycle (edit, compile, test) is a pretty big factor during early stage development. Running a test in a second versus running a test in ten seconds would ruin my day. I'm not sure what the current speed of compilation is for Rust but the only time that I looked at it (very early on) it was so slow as to be unusable from my perspective. I'd hope that has substantially improved. Think of what I'm doing as explorative programming, I'm both trying to understand the problem and trying to solve it at once.
> Understandable, but writing exploitable code because you don't know a suitable alternative language very well is not a good long-term situation.
That may be true. At the same time, Rust may not be a good long term solution either, languages come and languages go, and before I invest a year or so into a new eco-system I'd like to see it has staying power. It is interesting that you recommended Rust and not say Java which has pretty good performance, is memory safe and is in production for well over a decade, for this particular use case I would choose that over Rust.
> Define "immature". It's not one of the world's most mature languages, but it is definitely mature enough to write production code for many contexts.
There isn't a month or we see an announcement of the next point release of Rust on HN. I don't have the luxury of tracking a moving target next to working a full time job and having a family, then doing this hobby project besides. If Rust wants to see mainstream adoption for stuff like this because you seriously believe that writing a new project in C is irresponsible (which smacks of FUD, and is one of the reasons why I think Rust is one of the more toxic language communities on the internet, I have never seen a Java proponent use that kind of language) then I hope you agree that getting Rust to some kind of long-term stable form in the very near future is a must. This whole 'the sky is falling' tactic is rather off-putting, at least, it is to me. The same happened to perl, which was supposed to be - and still is according to some - the best thing since sliced bread.
> There are Rust libraries for all those things (including Rust bindings for FFTW itself), though I can't speak to their quality.
That's good to hear.
> I can confidently say that using Rust libraries in a new Rust project is a lot easier than using C libraries in a new C project
For you, as a Rust user, yes. But you are forgetting that for me that is not at all easier, and that using C libraries in a new project is a lot easier than using Rust libraries for me because I happen to know how that works.
Vantage point is a bit of an issue here, I take it that you are fluent in both Rust and C and that you have decided to hitch your wagon to Rust. I'm fine with that and no doubt in the long run you will be proven right but to me it smacks of 'you should do as I do' rather than that it is the best for me.
Learning a new language just for some project is a very high degree of friction to add, it would slow me down tremendously and it might lead to the project being abandoned rather than progressing at a quite acceptable speed given my constraints in time.
> especially if you're not on Linux.
I wouldn't dream of developing software on anything else, but I totally respect other people's choices in their platforms.
Interesting. High school students and university students that my kids know are using Rust (in New Zealand, not some high-tech mecca).
> Think of what I'm doing as explorative programming, I'm both trying to understand the problem and trying to solve it at once.
OK. I might recommend Julia then.
> It is interesting that you recommended Rust and not say Java which has pretty good performance, is memory safe and is in production for well over a decade, for this particular use case I would choose that over Rust.
Sure, Java sounds like a fine choice. I didn't know much about your requirements when I suggested Rust. People often choose C because of strict performance or deployment constraints, which Java often can't meet, which makes Rust often a safer recommendation.
> There isn't a month or we see an announcement of the next point release of Rust on HN.
That's because they release every six weeks. That doesn't mean the language is unstable or you will keep having to make changes to your code. It means there's a steady flow of incremental improvements that preserve backwards compatibility.
> you seriously believe that writing a new project in C is irresponsible (which smacks of FUD, and is one of the reasons why I think Rust is one of the more toxic language communities on the internet, I have never seen a Java proponent use that kind of language)
In hindsight I shouldn't have mentioned Rust at all. I do sincerely believe that propagating C code is irresponsible and the software industry needs to recognize this. It sounds harsh, but I think that's partly because software developers have historically taken too lightly the consequences of their choices (even for hobby projects).
People like me who are zealous about the industry moving away from unsafe code tend to be big Rust fans, because Rust finally makes that possible for the systems/embedded domain where C/C++ were for such a long time the only viable option. I guess we have skewed the Rust community to some extent. Mea culpa.
> it smacks of 'you should do as I do' rather than that it is the best for me.
When we write code and distribute it, we have an effect on the world, and then I think it behoves us to consider more than just "what is the best for me".
For hobby projects where the code is never distributed, these issues are mostly moot ... though it's amazing how often such projects escape sooner or later.
I believe that you are sincere in this. At the same time I ask you to recognize that the way you - and many other proponents of 'safe' (safe between quotes because computer programming will never be safe) languages approach this is combative and therefore ultimately un-productive.
The old proverb says that you will catch more flies with honey then with vinegar and headbutting with people and calling them irresponsible is not going to get you where you want to be. It will have the exact opposite effect, people will dig in and their resolve will strengthen rather than weaken. This is because people tend to be invested in their tools and their creations. Going for a full on frontal confrontation about this is counter productive, that's just human psychology, which is in cases like these as important - if not more important - than having a technological edge or being right.
Zealotry is the last thing you want to take with you in your toolbox if you want to make change.
To be fair other languages where there first, they just lost due to UNIX uptake across the industry.
Had any of those language been more sucessful regarding adoption across mainstream OSes, and a language like Rust wouldn't be required.
GC is a problem for OS/embedded programming. And before Rust, safety required GC (or an unpalatably restrictive programming model like MISRA). Even if Symbolics or the C# Windows Vista work had been more successful, Rust would still be needed.
You also wrote: "Not pretty, known to hurt you if you abuse it but still plenty sharp and well capable of doing the job if you treat it well."
To me it seems you know it can be dangerous, but like it too much not to use it.
I could have chosen Java, Python, Go or C++ instead. Instead I chose the tool I'm most familiar with because that will get me focused on the problem rather than on the tool. It's a disadvantage of getting older: there is less time left to waste.
> To me it seems you know it can be dangerous, but like it too much not to use it.
It is mostly a familiarity thing, not a like or dislike.
The ones who like C and C++ are like Toretto from the Fast and Furious franchise choosing a classic muscle car for a race. They are super fast, give you much more options, but because of that, they are not the safest.
A muscle car requires more skill to drive and win than a modern fast-shiny-and-safe car. But because you know they are more dangerous to drive, is likely that you will try to improve your skills to make it less likely to have an accident with it.
Thats one of the reasons why im still skeptic to jump into the Rust bandwagon, once you turn into a good muscle car driver, its hard to choose for safer but more limiting options, but you totally get it why someone without experience in fasters cars would choose for the safer version.
For actually getting things done there’s go and python of course.
"Oh but it's powerful", yes, a chainsaw is powerful but even that has an emergency brake. C has none of that.
(Not to mention compilers that take "undefined behaviour" as a code word for "bamboozle time")
So I have to hope for the best when some piece of hardware, coded in C, gets plugged into the network.
Morris Worm is 30+ year old, and yet best we can do is having platforms like CHERI, Solaris SPARC ADI, ARM MTE, which also happen to be quite specific.
So you'll be hoping for the best for some time to come, no matter what the stuff you install was written in.
Σ UB + Σ memory_corruption + Σ logical_errors ≥ Σ logical_errors
But every language that can reach the hardware will have the ability to wreck things in spectacular and hard to predict ways. Every new language ever was touted as the one that would finally solve all our problems. For Java and COBOL the historical record borders on the comical. I have no doubt that the same will go for every other language that we just haven't found the warts in yet. Two steps forward, one step back, that seems to be the way of the world in the programming kingdom.
Sending digital signals over analog medium (read: always) can fail, and may do so regularly depending on hardware itself and environment conditions.
Simple examples: Digital 3.3V signals sent over 10m, high-capacity lines (lose a bit here and then), or an overclocked CPU undervolting just at the right time, etc...
EDIT: I'd also argue that UB is a type of logical_error. Not the fact that UB exists within the language, but in that the logic failed to account for the scenario?
UB is its own kind of error, specially since ISO C documents over 200 use cases, and unless you are using static analysers, most likely won't be able to find out that are failing into such scenarios as no human is able to know all of them by heart, whereas logical errors are relatively easy to track down, even by code review.
Unfortunately liability is not yet a thing across the industry.
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=stack 3496 entries https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=pointer 2389 entries
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=java 2034 entries
A possible outcome is that you would trade pointer bugs etc. for "Java bugs" if Java embedded were used everywhere. Embedding a complete runtime increases the attack surface alot.
Many of those Java exploits are on C and C++ written code layer, yet another reason to get rid of them in security critical code.
According to Microsoft Security Research Center and Google's driven Linux Kernel Self Protection project, the industry losses due to memory corruptions in C written software goes up to billions of dollars per year.
Several reports are available.
Would you like to tell us how it compares to e.g. Java's runtime as well?
Then, unless we are speaking about a non-conformant ISO C implementation for bare metal deployments, it provides the initialization before main() starts, floating point emulation, handling of signals on non-UNIX OSes, VLAs.
I have been at this since the BBS days, I don't care about Internet brownie points.
The only thing left is actually having some liabily in place for business damages caused by exploits, I am fairly confident that it will eventually happen, even if it takes a couple of more years or decades to arrive there.
I would never think of "floating point emulation, handling of signals on non-UNIX OSes, VLAs" as anything resembling a "runtime". These are mostly irrelevant anyway, but apart from that they are just little library nuggets or a few assembly instructions that get inserted as part of the regular compilation.
By "runtime", I believe most people mean a runtime system (like the JRE), and that is an entirely different world. It runs your "compiled" byte code because that can't run on its own.
Dishonest is selling C to write any kind of quality software, specially anything conected to the Internet, unless one's metrics about quality are very low.
Assume whatever you feel like.
> 70% of the vulnerabilities Microsoft assigns a CVE each year continue to be memory safety issues
https://msrc-blog.microsoft.com/2019/07/18/we-need-a-safer-s...
If you are going to argue that is 'cause Windows, there are similar reports from Google regarding Linux.
Punting all of this extremely important reasoning to "the programmer" is ridiculous when it's been proven over and over again that even incredibly smart C programmers get this wrong.
Why not disallow undefined behavior and instead have the compiler emit: "Hey, this branch is never taken, if you want us not to compile it remove the dead code". Seems like that's exactly the same sort of putting the onus on the programmer that andrepd mentioned, but without the side effect of millions of device drivers are zero day remote code execution vulnerabilities waiting to happen?
Even Rust made a mistake with restricting undefined behaviour -- they accidentally allowed creating references to fields in packed structures (resulting in possibly unaligned references which is undefined behaviour in Rust) in safe code. These days you get a warning telling you to use unsafe or copy the value, but they have yet to make it an error. For comparison, C also gives you a warning for the same problem (or an error with -Werror).
I also want to point out that modern compilers have different sanitisers (-fsanitize=...) which cause your program to abort() on memory unsafety or other violations. It's effectively a better valgrind which is compiled into your program.
I don't know how anyone who has studied the history of vulnerabilities and security issues and code would think this is a hard tradeoff:
> If yes, your code is now slower but there isn't an easy way to disable this "feature" (if x is heap-allocated or passed as a pointers, how will the compiler know what kind of bounds check is sufficient?).
> If no, you have undefined behaviour and crashes.
The latter choice has resulted in how many millions or billions in losses, perhaps even lives lost?
Okay, maybe you can add a small overhead to array accesses (which is what Rust did), but you can't stop undefined behaviour entirely. Rust has plenty of undefined behaviour (most can only be triggered through unsafe -- which is the whole argument behind the language -- but as I mentioned there are some cases where even they made a mistake and made an operation safe when it should've have been).
In C/C++, undefined behavior and unsafe operations abound and it is nigh impossible to isolate what is "important" to review from a security standpoint.
You can't "disallow" all undefined behavior in C without fundamentally changing the language to something different (Which would also likely make it useless for it's intended use-case). Some of the details related to undefined behavior are simply not possible for the compiler to detect without extra information, hence why they are undefined. `Rust` has the same thing, you can invoke undefined behavior anywhere in the program through use of `unsafe` anywhere else. And Ex. How is the compiler supposed to determine if a particular function is ever called with a `NULL` parameter? How does it know if a particular signed addition may overflow? How does it know if two parameters to a function alias in an invalid way? How does it know if a particular pointer lacks a `const` and is actually backed by read-only memory? And most importantly, how does it determine all of this at compile time?
With that not all undefined behavior-based optimizations are unexpected or warrant a warning or error (In-fact, I would say at the very least the majority of them don't). Undefined behavior is much more complicated then just dead-code removal. Even then though, you don't always want dead-code removal warned about - Imagine getting a warning about every inline function or macro that results in some dead-code.
To be clear, I'm not saying there aren't obvious problems that have come from this approach, and I think some of the choices for undefined-behavior are clear mistakes now even though they made some sense in the 80s or 90s (And some of those can usually be turned off, thankfully). And I think there are also some additions that could be made to the language that could solve a lot of the existing problems (Like non-null pointers, or fat pointers, though I'm not exactly holding my breath...), but the existence of undefined-behavior is not in-and-of-itself the actual problem with the language, and "disallowing" it outright is simply not possible. I would actually wager undefined-behavior exists in some form in a lot more languages than you'd expect, just less documented. Rust certainly still has it, despite it's numerous safety-focused features. And even if you add things like fat pointers to C that would clearly reduce possible out-of-bounds mistakes, the language would still need to provide some way to create one from a size and a pointer, and that still allows for undefined-behavior cases to happen - my point being, while adding such a thing is still clearly better, it does not actually remove the undefined-behavior possibility.
It's not very expensive because the branch will never be taken except when it results in an out of bounds access. When that out of bound access happens you care more about the security benefits than performance.
I'm curious if this still stands in general today, given today's advanced branch predictors and the fact that the CPU tends to be memory-bandwidth-bound, thus you have more "free computation" while waiting for memory (what GPU programmers call compute to memory ratio).
C is fantastic for small and very optimized components. If you want it portable, from microcontrolers to GPUs, it is the best choice too.
But if you need rapid prototyping, large scale projects within an organization or some libraries and tools to make the perfect, animated, templated and themed user interface you should think twice before choosing C. Java and .Net are freaking good at database integration and memory allocation/dealocation. Erlang is fantastic for multitasking and multithreading. Ruby on Rails or Python on Django are much better to prototype a website nannying a database.
You already know the saying "if your only tool is an hammer..."
The bottom line is to echo your first statement, C is a tool, good for some tasks, not so good for others. Choose accordingly.
Doing it in C obviously. I can’t really consider Go embedded because I’m maxing the chip out now in C without including a Go VM or whatever you call it.
This isn’t all as rare as you might think.
If every feature everyone wanted was added to C, it would just turn into a bloated mess like C++, or like PHP, with tons of bad decisions baked into the language that can't really be avoided. Features like this belong in libraries - the stdlib should remain as minimalistic as possible.
The C++ evolution is essentially an iterative cycle where people hackily create new language features with insane preprocessor and template hackery, and then the Standards Committee comes in and takes the most popular hacks and tries to turn them into real language features. And then people create new hacks on top of that.
C says, you want object oriented polymorphism, you've got structs and function pointers, what more do you need?
No, C says your first mistake was wanting object oriented polymorphism. Your second mistake will be implementing it.
These programs, by the time I came aboard and started sharing code with those to make the games I was involved in, had also "succumbed" to this force.
The shared bedrock of all these games was code that used certain Z80 registers as pointers to take advantage of instructions that read or wrote fixed offsets from those pointers. The first byte of the record pointed at by the pointer was the type and the next 4 bytes were the position. The rest of the record depended on the type.
That was us doing the best we could with 8-bit processors when our competition was using 68000.
Edit: Rather than responding to all the comments individually: There's obviously a lot of code already written in C. That doesn't mean it makes sense to write new code in C. Sometimes C is your only option, which I already said in my original comment.
None of that means there is any reason to start a new project in C if it's going to run on Windows or Linux, and it's certainly not the case that you have to write your program in C if you want to call it from another language. Sorry if this disappoints anyone, but it's true. For the traditional uses of C, such as writing a text editor or a library for numerical computing that you're going to call from another language, there's no longer a sensible argument for using C.
Linux (like the BSDs) being C is an organizational phenomenon. There is no plausible technical reason for it not to be, or include parts in, C++. History is path-dependent; how we got here matters more than what is around today.
There is really no excuse for systemd being C. It is really a drag on its development.
On the OSes that aren't just plain UNIX clones, C has a much less relevant role, and in some of them it is even being phased out, even if it might take a couple of decades yet.
Pascal UCSD also used a mix of interpreter and later AOT compiler.
Most mainframes don't use C as their original implementation language, and the oldest still being sold (Unisys ClearPath MCP) was designed about 10 years before C was born, in an Algol derived language, that uses instrics instead of Assembly.
If one broadens their horizons beyond UNIX, and looks into the history of computing there are plenty of interesting languages and OS architectures to dive into.
The Internet is full of digitalized version of papers and computer manuals from those days.
What happened instead was that everybody had their own Pascal dialect with the extensions it needed to be actually useful. Meanwhile, C had them all, already, and was about the same everywhere. Soon after, C++ came along, and there was again only one (although Microsoft delivered workable template support very late).
Lesson in Network Effect there. Also, shooting for usefulness out the gate.
Meanwhile, the Pascal crowd went off with Modula-2, Modula-3, Oberon, Apollo Pascal, UCSD Pascal, Object Pascal, Delphi, and countless others, none compelling. Apparently Delphi (still) has economic importance. The rest, not so much.
This part isn't quite true. All that's needed is to be able to define an exported function with a specific name and calling convention. Libraries can be written in any language that supports these features.
Even C++ is a pain in the behind, because everything that doesn't translate needs to be encapsulated.
As a result, it's very rare to find code written in different languages with exported C APIs from my experience.
Add name mangling, ABI issues and crazy compile times to that.
It was potentially safer until they added Exceptions, which tipped the scales so far the other way I can't believe we're still having this discussion.
But you won't when writing C++ unless you absolutely have to, because it's a major pita to encapsulate everything.
Entire books [0] have been written about exception safety in C++. Getting it right in combination with RAII, (copy)-constructors and assignment operators is very tricky. Which touches the main issue with C++ from my experience, mixing foot guns with higher level convenience.
Embedded linked lists [1] is a good example.
Seriously, live and let live. I'm fine with you enjoying and preferring C++, go for it. But consider taking a good look in the mirror and asking yourself why you feel the need to spread FUD about C to feel good about your choice.
[0] http://www.gotw.ca/publications/xc++.htm [1] https://github.com/codr7/libceque/blob/master/source/libcequ...
The key is to make sure all your cleanup code is in destructors where it will be exercised frequently as part of the normal operation of your program. In the event an exception is thrown, a bunch of destructors run, as usual, and your program ends up in a known clean state, with no extra effort.
Rust is the best candidate, but it operates on a completely different level. (That said, I don't think one ever need that much control on a PC or a server. Not even on kernel programing.)
There are other modern alternatives nowadays as well, including for MCUs.
C has not evolved because C++ has, and because the computers got a million times faster, so even Python is usually good enough. If you need something that does what C does, but better, C++ is right there waiting. Change your makefile, or rename some files, and suddenly you have a C++ program, that is then easy to improve as much as you like.
Well, it has, in a way: see the latest C standard and compare it with the Kerninghan&Ritchie edition.
That's what you think. Transistors have evolved a lot, as have programming languages.
It's the only language your chip vendor will give you, but there is very little reason ever to use their compiler, anymore. And they have no say in what you use to generate your machine code.
Anyone that is open mindend can get Pascal, Basic, Java, Oberon, C++ and Ada compilers, although I admit that the Ada option is only available for deep pockets.
Several mistakes in c:
Using string interpolation preprocessor macros makes it so that you have to 1) learn a whole second language to read other people's code (most portable code requires using #defines) and 2) it's very difficult to trace through code where you don't know where some values are coming from.
Header/code segregation seems like a huge mistake. It's far more effective to have a single file with well-defined exports. Even the #include system is broken, because it's not explicit or obvious which directories one must go to to find header files (they might not be local to your project)
Sigil order. The fact that there is a guide saying you should "spiral out" to understand how to read a type should be evidence enough that this is a huge mistake.
I get it that there are professional c coders that will say that's part of knowing the language. I'm not a professional c coders, I program in another language and very rarely need to dip into something lower, say for performance or mutability. Thus the c code that I should write is dangerous, I need safety, and simplicity. Luckily, I found another language that hits those spots.
There are lots of small towns still emptying out. The people who remain are more likely to say they like them.
I decided to work on this year's Advent of Code in C and it's been a blast so far. I finally "get" pointers, malloc/free, etc. (and I have a newfound appreciation for Ruby things like CSV.each).
Since most of my coding work is at a much much higher level (web backend & frontend stuff), I probably won't spend too much time in C in the regular course of business, but it's massively helped me better have an intuitive understanding of previously vague things like "allocate on the stack vs. heap" and "pass by reference vs. value" -- I knew what those words all meant but didn't really have a feel for what they actually did. C helps with that.
If you want to write a new programming language that does the same from the get-go, make C as one of the compilation targets.
As other commenters have pointed out, use the right language for your use-case. Then, I agree that exporting a C interface is best.
Got as far as playing around with a web site that used Rustler NIFs on a Phoenix website. Why? It was just fun to figure out how to get something from the web frontend through Elixir and into some Rust numerical code.
I don't think it had much practical use and knowing me, would be ridiculous to go back and touch it again (likely). I've been thinking about doing Rust to WASM but I'm still not sure if it's just throwing a bunch of complexity at something just because. It holds my interest a lot more than just doing everything in JS, though.
C was also more enjoyable to me than other languages. Something about compiled languages just feels really nice. But, it's also great to have a REPL available right there. Like with Elixir.
But the main issues that I've had with Go is when the Go runtime is causing your code to very much not act like a C program. To be fair, I work on container runtimes and similarly "lower level" tooling so this is probably not an experience that everyone else has.
So... what is the equivalent of doing this in C?
* install C compiler (no, there is no install script, go google and install yourself and yes, the process is different on different platforms)
* read a blog post (possibly on medium) and write hello.c
* read another blog post to figure out compiler flags
* ./a.out
A bit less convenient, but not that different
https://www.miasap.se/obnc/oberon-report.html
I know you can find bolt ons, but they aren't terribly useful since no 3rd party libraries use them. You have to resort to stuff like antirez's sds.
https://queue.acm.org/detail.cfm?id=2010365
That's not to say that the rest of C is bad, but there's nothing to be gained from defending C strings; they're bad and you should probably use an alternative and that alternative should probably have a length and a buffer, even better if you can find this out of the box in your language that otherwise works a lot like C.
That then implies at minimum a sanitation of user- and data-input of char array, and maybe an absolute maximum overrun char count protection at execution, but once your default is to keep track of every string length and not the opposite of keeping track of specific strings, I'd kinda agree with above that it's not really "C"... more like your own implementation of a(n overrun-safe) string library?
Inherent to the concept of the char array is that a size value would be redundant, for it is the position of the null-terminator that already contains/is this.
The analogy for the string concept of a char array's missing null-terminator is actually a wrong size value.
I ask myself, why are strings safer?
But I think "getting close to the lower levels" is still great. The only time I write C-like code is for an Arduino. Turns out that even if your electronics is rusty, you can do some very useful things in the real world by automating a simple motor and a few LEDs. And whenever you have to interface with individually numbered output pins, it feels pretty low level.
tldr: if you want to get close to the metal, it's really useful to start by switching to hardware that facilitates this.
It's not like playing with an Arduino is getting me closer to understanding the underlying chemistry and physics. Which are also interesting to learn about...
But personally I think it's for the best that reality is inexhaustibly complex (from the perspective of human individuals).
Since you mention the Arduino, when you start out with it, you're most likely turning on digital pin e.g. D7 with digitalWrite(7, true), it's easy to understand and quick to implement.
But spend enough time on the Arduino (or C or Assembly or embedded in general) and you'll soon realize that all that function call is doing is basically the same as doing PORTD |= 0x80. That's a bit tougher to understand, but even quicker to implement, and once you've understood how that all works together, a lot of everything about computers and digital electronics becomes so much easier to understand.
Here's a more detailed guide for the Arduino example: https://www.instructables.com/id/Fast-digitalRead-digitalWri...