Live data from Hacker News

Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

cl.cam.ac.uk

191–200 of 253 posts

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#191
Hi,

I think C's success is also because it was designed while solving real problems when writing UNIX. Unfortunately the newer languages that claim to be "systems" languages were not designed while building operating systems. Here is Dennis Ritchie's assessment of reasons for C's popularity:

(Extract from http://csapp.cs.cmu.edu/3e/docs/chistory.html).

C has become successful to an extent far surpassing any early expectations. What qualities contributed to its widespread use?

Doubtless the success of Unix itself was the most important factor; it made the language available to hundreds of thousands of people. Conversely, of course, Unix's use of C and its consequent portability to a wide variety of machines was important in the system's success. But the language's invasion of other environments suggests more fundamental merits.

Despite some aspects mysterious to the beginner and occasionally even to the adept, C remains a simple and small language, translatable with simple and small compilers. Its types and operations are well-grounded in those provided by real machines, and for people used to how computers work, learning the idioms for generating time- and space-efficient programs is not difficult. At the same time the language is sufficiently abstracted from machine details that program portability can be achieved.

Equally important, C and its central library support always remained in touch with a real environment. It was not designed in isolation to prove a point, or to serve as an example, but as a tool to write programs that did useful things; it was always meant to interact with a larger operating system, and was regarded as a tool to build larger tools. A parsimonious, pragmatic approach influenced the things that went into C: it covers the essential needs of many programmers, but does not try to supply too much.

Finally, despite the changes that it has undergone since its first published description, which was admittedly informal and incomplete, the actual C language as seen by millions of users using many different compilers has remained remarkably stable and unified compared to those of similarly widespread currency, for example Pascal and Fortran. There are differing dialects of C—most noticeably, those described by the older K&R and the newer Standard C—but on the whole, C has remained freer of proprietary extensions than other languages. Perhaps the most significant extensions are the `far' and `near' pointer qualifications intended to deal with peculiarities of some Intel processors. Although C was not originally designed with portability as a prime goal, it succeeded in expressing programs, even including operating systems, on machines ranging from the smallest personal computers through the mightiest supercomputers.

C is quirky, flawed, and an enormous success. While accidents of history surely helped, it evidently satisfied a need for a system implementation language efficient enough to displace assembly language, yet sufficiently abstract and fluent to describe algorithms and interactions in a wide variety of environments.

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#192
post #181

Earlier quoted context omitted.

> Even such a simple use case is fraught with major problems: > > 1. who allocates needed memory? > > 2. who free's it? That's also a major feature. It allows people to write systems that are resilient in the face of tight memory limitations. It's not cool when a language forces string operations to allocate & duplicate memory willy-nilly. > 3. can the compiler constant fold cat("hello","world") ? Does the result win…

It's the opposite. I've seen lots of code written in C that pretends to be out of memory safe. I've never once seen such a program that actually is out of memory safe. Invariably the codepaths triggered by malloc returning null are never exercised. With a GC and exceptions you can theoretically be quite resistant to OOM conditions, not that anyone really cares.

One of the things with tight memory systems is that you don't use malloc to begin with, if you can avoid it. C gives you the option.

When you're concatenating strings, you already have storage for those strings. Maybe you can re-use that storage. Maybe you have a static buffer. Maybe you have a fixed size buffer on the stack and the stack use is bounded.

A language that forces you into making redundant duplicates onto the heap is terrible in these situations.

And yes there are programs that try to deal with failing mallocs. Again, C gives you the option.

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#193

Earlier quoted context omitted.

I think I understand. The idea would be to have one HashMap that holds the objects themselves, and then a secondary HashMap (with a star this time) that indexes on some other key and points to values stored directly in the first map? What's the benefit of doing that, compared to making both the HashMaps store pointers to independently allocated objects on the heap, such that insertions into one map never invalidate t…

> Is the hope to avoid paying the cost of an extra pointer dereference when we're using the first map? Or does independently allocating each object hurt cache locality or something like that? Both. In practice, I probably wouldn’t use a hashmap for the first container that actually owns these items. When I do expect gigabytes of data, in C++ I use something like vector >, where the inner vectors are of the same fixed…

Right, and I don't understand why you think that same exact approach wouldn't work in Rust either. If you have a `Vec>`, then you can spread all the raw pointers you want everywhere without any additional boxing of `tValue`, and you know exactly when those pointers might become invalidated: whenever you call an `&mut` method on your `Vec>` (or rather, on one of the interior `Vec`s). Because of that, you can even build safe abstractions on top of such data structures such that your callers can't possibly misuse it (without themselves using `unsafe`).

The technique of giving stable addresses to things by stuffing them into vectors isn't unique to C++. People do it in Rust too: https://github.com/SimonSapin/rust-typed-arena/blob/master/s...

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#194

Earlier quoted context omitted.

From the link: "That means that we have been going through the tree cleaning out all calls to sprintf(), strcpy(), and strcat(). Instead, these things are being rewritten to use asprintf(), snprintf(), strlcpy(), and strlcat()." Maybe the author made a typo.

Oh yeah, you're right. Another thing I've done that will work if you have a lot of strcat(), is make a string struct: ktString { int len; int memlen; char *str; } It keeps track of the string's actual length, and the size of the underlying buffer. Then you can 'override' the various string functions: bool ktStrcat(ktString s1, ktString s2); bool ktSprintf(ktString s1, ...); These functions will take care of buffer-si…

... and end up with silent truncation unless you happen to always remember to use only C library functions with explicit length arguments (and which do not assume NUL-terminated strings).

Look, I get that there is a place for C, but string manipulation is absurdly bad and error-prone.

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#195

You can pry gcc out of my cold, dead hands when your fancy type-safe high level languages will let me do things like: * Fork a running program to enable analysis or serialisation of program state without blocking, or * Use mmap to allocate all my datastructures on disk, or * Have full control over what happens when my program receives a signal, or * exec another program but have it to inherit all the open file descri…

I regularly do all of those things . . . in Perl, with either core features of the language, or ubiquitous, well-supported libraries, in readable, concise code that doesn't "fight" with the language/runtime.

Something like Java definitely makes some of those things very hard née impossible, but not all high level languages are the same in those regards.

(And yeah, I just called Perl "readable". Bite me.)

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#196
post #189

Earlier quoted context omitted.

Unique pointers provide no protection against use after free, because you can take a reference to their contents and that reference can become dangling. Because the destructor of a unique pointer is invoked automatically per the language rules, as opposed to in C where an explicit call to free is required, this makes C++ more prone to UAF than C.

> Unique pointers provide no protection against use after free, because you can take a reference to their contents and that reference can become dangling. Yes, it's possible, however references should only ever have local scope so, unless you're dealing with threads or asynchrony it's hard to write sane code where this happens, and if you have those things, and you're passing refs or ptrs, then I don't have to tell y…

> Mentioning free() just implies that you're willing to accept resource leaks to avoid UAF bugs, which is nuts because UAFs can be a lot easier to debug.

If you're focused on security, it goes in the opposite direction: a resource leak can lead to a denial of service, but an use-after-free can lead to remote code execution, which is much worse. From that point of view, it's worth it risking a resource leak if by doing that you prevented a potential instance of remote code execution.

By the way,

> C++ destructors run after the last line of your code block

Aren't there many situations where the C++ destructor runs at the end of the current statement? IIRC, if you call a function which returns a temporary, then call a method on that temporary which returns a reference to within the temporary, and assign the result to a variable, all in a single statement, the temporary will be destructed while the reference to its contents is still live.

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#197
post #120

Earlier quoted context omitted.

There is no inherent benefit in C that, for example, a somewhat modified version of Pascal or Algol wouldn't have inherited. I happen to like C and think it has a lot going for it, but you're definitely right about this. The original Mac OS was written in Pascal (and assembly), not C. And Turbo Pascal was deservedly popular for a good while. In an alternate universe, Pascal rather than C could be the incumbent legacy…

You have to wonder, though, why the enduring big dogs of OSs are written in C. Classic Mac OS was written in Pascal, but it had to be scrapped and replaced.

It's pretty simple: C was already popular -- meaning: at least decent-ish compilers for most platforms.

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#198

Earlier quoted context omitted.

> This went ok, but I still have no idea which of the 6 string types I should use for a library like that. None of Swift, Go or C have this problem. There are two string types: a string that owns its contents and a string that references its contents. This is the same as in any language that uses smart pointers for resource management. Can you name a string type that you think should be removed, and explain why? > Fi…

> There are two string types: a string that owns its contents and a string that references its contents. This is the same as in any language that uses smart pointers for resource management. There's String, &str, Cow , Rc and other variants. None is canonical. I spent about 2 hours reading documentation trying to pick the right type to use and I think I ended up with Rc >. But in this instance my strings represent ch…

> There's String, &str, Cow, Rc and other variants.

To be fair, that's like saying std::string and std::shared_ptr are two different string types in C++, and that neither is canonical.

In Rust, String/&str are the canonical string types. String is an owned growable buffer, &str is an immutable slice. That's it. Adding Cow, Rc or Arc to the mix is orthogonal to the specific string type you're using. They are smart pointers and can work with various types other than strings.

> What I actually want is an efficient version of enum Str { ShortStr(char[X]), Ref(Rc>) }, but encapsulated behind a common string interface.

We couldn't get away with adding this as the standard library string type because it would impose non-zero costs on every use of a string. The use of Rc is particularly grating because it's not thread safe, which means you wouldn't even be able to send strings across threads. That would suck. So then you might want to say to use an Arc---atomic ref counting, thread safe---but that's even more costly.

I'm honestly kind of confused at your feedback here. At first it just sounded like you were bewildered by the various string types---which is a fair criticism, getting strings right is hard and everyone has opinions on what they should look like---but it actually sounds like you knew exactly what you wanted, and were frustrated that the standard library didn't have it. Instead, the standard library gives you a fundamental string type that one could use to build other more advanced string types when you need them.

The typical solution to problems like that is to go out and build what you need and put it on crates.io. Or, use one that already exists. :-) https://docs.rs/inlinable_string/0.1.8/inlinable_string/

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#199
post #171

Earlier quoted context omitted.

Tagged unions? -- please no... I could be convinced to completely lose unions in C, though (pointer to member or base of struct can be cast to another struct anyway, so losing unions doesn't gain anything; for the same reason tagged unions just would not be useful) Boolean type? Sure, but that would be dependent on use. What is wrong with a bitfield one bit wide instead? What may be useful is "packed bitfield" type (…

Unions would be nice if the syntax for accessing substructure members could be nominally short circuited. For example: struct ab { int a; int b; }; union c { struct ab ab_short_circuit; int a; }; union c c1; c1.a = 1; c1.b = 2;

That already exists, as a Microsoft extension, and if the struct is declared within the union, in standard C: https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Unnamed-Fields....

(However, in your example, c1.a is ambiguous, so it won't compile.)

Re: Some Were Meant for C: The Endurance of an Unmanageable Language [pdf]

#200

Earlier quoted context omitted.

> Is the hope to avoid paying the cost of an extra pointer dereference when we're using the first map? Or does independently allocating each object hurt cache locality or something like that? Both. In practice, I probably wouldn’t use a hashmap for the first container that actually owns these items. When I do expect gigabytes of data, in C++ I use something like vector >, where the inner vectors are of the same fixed…

Right, and I don't understand why you think that same exact approach wouldn't work in Rust either. If you have a `Vec >`, then you can spread all the raw pointers you want everywhere without any additional boxing of `tValue`, and you know exactly when those pointers might become invalidated: whenever you call an `&mut` method on your `Vec >` (or rather, on one of the interior `Vec `s). Because of that, you can even b…

The exact container is not that important here. The point is, C++ allows composing these containers making higher-level ones, such as this indexed array example.

They can be standard, third-party, my own, I still can compose them.

About my particular example, I’m not sure you can easily implement a free list in rust, to reuse space from de-allocated items. Especially if these items have non-empty constructor and destructor.

Post reply on HN