Dependable C
14 points by jado
14 points by jado
It seems very generic and beginner-level advice: try not to write UB, try not to make non-portable assumptions, try not to fall for the handful of other gotchas. Okay?
It's shallow and doesn't really tackle any real problems. It rejects C11 and suggests POSIX threads instead. Last time I checked, POSIX wasn't dependable in MSVC (in MSVC even fopen isn't dependable due to stdlib mangling Unicode filenames).
And the Why is C the safest language? is so confused. It scolds security researches for not using the scientific method, but the whole section is based on treating correlation as causation. Many "security-critical" projects use C despite its unsafety. For a long time, C was the only language that met performance and interop requirements such projects had, which forced them to accept the dangers of C and deal with consequences.
Its worth appreciating how seldom major security issues appear in major security critical software's written in C.
I don't know what to say to this quote from the article, so I'll say nothing.
scolds security researches
The author gave a talk last year with similar vibes: https://www.youtube.com/watch?v=SbeNRICgzTA
Of course it's at the better software conference, better described as C³: condescending contrarian conference.
try not to make non-portable assumptions
Often I wonder if we shouldn't write our platform-specific code and be done with it? (Document it with a special comment to grep for later.) C's never shy to leave things implementation-defined: I think the spec is trying to tell us something!
Defaulting to C89 is already enough for me to disregard everything else. C99 is by far a much better language.
Yeah, I feel you. At some point at my previous job I was given a suggestion to rewrite my C99 code to C89 for speculative reasons. I couldn't understand why I had to do this so I just couldn't do that. So I didn't.
Is this a thing people do just so that the c code will be compilable with a c++ compiler? since c++ doesn't have designated initializers and c code that takes advantage of c99 often will use them.
since c++ doesn't have designated initializers
C++ does have designated initializers as of C++20, but with differences in semantics. E.g. you must specify the field initializers in declaration order, like the member initializer list in a constructor, even if the type is trivially constructible. So it's possible (annoying but possible) to write C with designated initializers that can be compiled as C++ code while preserving the intended meaning.
But another issue is that many of the classic use cases for designated initializers also involve C99 compound literals. And when people say they want designated initializers they usually mean the combination of those two features. For example,
create_texture(
&(struct create_texture_info){
.width = 1024,
.height = 512,
.format = RGBA8888
}
);
uses both features. In C++ you'd use const references to accomplish the same thing.
Oh interesting, thanks. I didn't know c++ had added even part of it. Yeah I do always mean the combination of features that make c99 struct literals nicer than c89.
Historically, MSVC supported modern C++ but only C89 (albeit with some minor stuff like C++-style comments enabled). Modern MSVC does do C11 at least IIRC.
I spent most of my time working on Windows, and C99 support was only added to Visual C++ ... very recently.
While it's possible to depend on a newer compiler, doing so also implicitly means the compiled program will require a newer version of the operating system.
Obviously different people are free to form their own conclusions about the relative importance of things, but as I'm writing this there's another story on lobste.rs about somebody getting an Itanium Windows VM to run, and I'm confident my C89 code will at least compile on that system.
While it's possible to depend on a newer compiler, doing so also implicitly means the compiled program will require a newer version of the operating system.
Wait, why would a newer compiler necessarily imply the output will require a newer OS? Well, beyond newer compilers explicitly dropping support for older OSs, I guess...
The compiler is bundled with the C runtime library, and the C runtime library makes OS function calls. The calls it makes limits where the resulting code can execute.
But there's nothing that requires compiler outputs to rely on the same libc that the compiler uses/is bundled with, is there? You can cross-compile for other(/older?) libcs or even forgo libc entirely if you so desire.
But whats stopping you of using mingw/clang?
Also it seems that since 2015 visual studio supports most of c99.
Both C99 and C11 had big-ticket items that added a lot of implementation complexity. For C99, this was all of the Fortran-envy floating-point stuff. A lot of implementations didn't claim C99 compliance for a long time because they didn't completely handle all of this (GCC benefitted from the fact gfortran had previously required them to implement it in the language-agnostic bits). Almost no C code outside of scientific computing uses it because it's horrible (just read the specs for the floating-point pragmas and see).
C11 added atomics. These impact optimisers, but mostly by permitting things. Visual Studio already had a definition of volatile that, roughly, meant the same as sequentially-consistent atomic, and had library functions for all of the atomic operations, so adding C++11 atomics support was easy and surfacing them in C was not too difficult, but adding the optimisations that were now permitted by the new atomics semantics was more work (I'm not sure how much of this they did). Most of the other bits of C11 are fairly simple.
As an experienced C developer, I have a number of hypotheses as to why C has been so successful
...followed by nonsense. C became common because Unix became common. C stays common because there's a lot of C already out there, Unix provides calling conventions that break down for anything more complicated than C, and you pretty much could only assume, for a long time, that a Unix machine might have a C compiler and maybe Perl. At the same time, Unix assumes that you're building things from source, so questions about binary compatibility get ignored until crises happen (libc transitions in the Linux community come to mind).
You can write stuff in other languages just fine, but if the language has semantics around functions that are more complicated than "here's a stack frame, look up an offset from a fixed symbol table and jump the instruction pointer," then Unix doesn't really support it. That's not an inevitable limitation. Windows has had COM since the 1990's, for example. So you can interop with Fortran from anywhere on Unix as easily as you can with C (GNU Fortran appends an underscore when mangling function names).
There are times when I would start a project in C, but they would be entirely because of toolchain and compatibility issues that forced that choice. I don't think there's a time in the last fifty six years when it's a language I would have chosen on its own merits.
You can write stuff in other languages just fine, but if the language has semantics around functions that are more complicated than "here's a stack frame, look up an offset from a fixed symbol table and jump the instruction pointer," then Unix doesn't really support it. That's not an inevitable limitation. Windows has had COM since the 1990's, for example. So you can interop with Fortran from anywhere on Unix as easily as you can with C (GNU Fortran appends an underscore when mangling function names).
This is less of a function of COM (which is mostly just vtables and reference counting) and more a function of a language-neutral ABI.
C had a big boost because of Unix, but to say it became popular just because of Unix is reductive:
https://utcc.utoronto.ca/~cks/space/blog/programming/CTriumph
This significantly understates the real appeal of C at the time, even and especially to people who had alternative languages. A great illustration of this is C on the early Macintosh. You see, unlike environments like MS-DOS (which had no language associated with it, just assembler), the early Macintosh systems already had a programming language; they were designed to be programmed in Pascal (and the Mac ROMs were originally written in Pascal before being converted to assembler).
This was more than just an issue of Apple's suggested language being Pascal instead of C. The entire Mac API was designed around Pascal calling conventions and various Pascal data structures; it really was a Pascal API. Programming a Mac in C involved basically swimming upstream against this API, full of dealing with things like non-C strings (if I remember right, Mac ROM strings were one byte length plus data). I believe that Mac C compilers had to introduce a special way of declaring that a C function should have the Pascal calling convention so that it could be used as a callback function.
Despite all of this, C crushed Pascal to become by far the dominant programming language on the Macintosh. I don't think it even took all that long. Programmers didn't care that dealing with the API issues were a pain; working in C was worth it to them. It didn't matter that Pascal was the natural language to write Mac programs in or that it was a perfectly good language in its own right. C was enough better to displace Pascal in a hostile environment.
A 10 year old Python library is unlikely to run in the version of Python you run without constantly being maintained.
On the contrary, after the python2 to 3 migration, the backward compatibility in the language has been good. I would expect 10 year old python libraries to just work.
There have been some significant breaking changes in 3.x versions over the past 10 years, e.g. some removed standard library modules. I got hit with distutils being dropped a few years ago (I would specifically use the strtobool function whenever I needed a less naive y/n prompt, and then had to copy the code from the old version myself). I think whether a 10 year old Python library would work as expected depends on what it did, and how complex it was (and how diligent about specifying dependencies).
It's a pain that it's not there by default, but pip install setuptools will still get you distutils
I'm not sure what to think of it. It doesn't read like it's written by a knowledgable engineer:
Basic assumptions about Common platforms that are fair to make.
- Types are aligned to their size.
Which would imply struct{ int a, b, c; } is aligned to 3 * sizeof(int) instead of int, so an alignment of 12.
*int is at least 32 bits.
Which excludes all 16-bit embedded platforms, making the code unusable for them.
C++ is not a subset of C
Well, who could've guessed. Neither is C a subset of C++.
Don't ever read memory with a type that is different then it was written.
So never serialize/deserialize data, as i cannot read a float as 4 bytes?
There's some good and some bad advice in there, but overall i would not recommend following this guide.
One of the greatest strengths of C is its compatibility. C has more implementations than any other programming language, more existing code, more documentation and more experienced programmers than any other language. The cost of breaking all of this compatibility, is simply higher than the value brought by any improvements in the language.
Back not too long ago, but long enough to fall from memory, the language people were saying this about was FORTRAN. Not Fortran, but line-number column-formatted all-caps mainframe-DP EBCDIC character set FORTRAN. FORTRAN 66 was not going anywhere... until it did, eh?
(Yes, yes, it still hasn't gone anywhere, I fully expect to be puppy-piled by the dozens of people who make their livings maintaining grand-pap's FORTRAN 66 what is vital to the business processes of a FORTRAN 200 company. It still isn't the 800 lb gorilla it was.)
The unpleasantness of FORTRAN 66 is why Brian Kernighan wrote ratfor, which supported the Software Tools suite of unix-like utilities on IBM and other systems that only had Fortran. (They were relatively popular in the 1970s and 1980s.) And there’s Brenda Baker’s struct which could turn unstructured Fortran 66 into reasonably well-structured ratfor, well enough that programmers thought struct’s output was often better than their handwritten Fortran.
Isn’t this so-called strength its greatest weakness if you squint? It’s multitude of implementations belies its cumulative lack of specification, yes?
C implementations are reasonably compatible, with enough preprocessor macros.
One of the great remaining advantages of C is that there really is a compiler for almost anything, including some of the most godforsaken and obscure processors out there. Granted, that compiler might cost $5,000 a seat and support most but not quite all of C89. You might feel dirty after using it.
To be fair, running on weird 90s-era DSP chips (or stranger things) is a niche choice these days.
While writing this up, I was pleased to discover that someone had successfully built a C compiler for the 6502, which was famously difficult to make run C when I was younger. So, yeah, unless you get down to deep embedded chips with 256 bytes of RAM, you can find a C implementation.
(Not that any this, in my opinion, excuses using C for new software. With a half dozen exceptions, every C program written has security holes you could drive a truck through.)
I thought the problem with C on 6502 was not to get it to run, but rather to get it to run performantly; CC65 has existed for a long time now after all.
So, yeah, unless you get down to deep embedded chips with 256 bytes of RAM, you can find a C implementation.
Pretty sure a lot of ATTinys have 256 bytes or less of RAM.
CC65 has existed for a long time now after all.
Huh, I missed that one! Thank you. I had moved on from my 65C02 days when that came out, except for one moment of nostalgia in 92/93, where I had a student job keeping an eye on an antique Apple IIe lab, and trying to write some AppleSoft BASIC from memory. I think I got a version of "Snake" working.
I see ATTiny models actually run from 32 to 1024 bytes of RAM. And yeah, those do have C compilers! Impressive, though doubtful whether you really want true C with 32 bytes of RAM.
It’s multitude of implementations belies its cumulative lack of specification, yes?
No, because unlike with Python, there are actual written standards out there and implementations tend to hew to them pretty well, especially old ones like C89.
Isn’t this so-called strength its greatest weakness if you squint? It’s multitude of implementations belies its cumulative lack of specification, yes?
The more I read, the less I liked it. In the list of keywords to avoid, it missed the C++ class keyword---you know, the point of C++! The rational for assuming (or preferring) little endianess was silly; it didn't even have a technical computer reason behind it. The argument about type sizes only mentions 128-bit pointers? Nothing about sizes being implementation-dependent and that the minimum sizes relate back to the 70s. And the entire "tweet" section just comes across as gatekeeping to me. Yuck.