9 ms·
My review of the C standard library in practice
- celeritascelery 4y agoThis just goes to show how hard it is to create a API to last 30 years. So much has changed since then, but libc needs to keep the same API. Things like Unicode and the internet (which brought the need for security to forefront) have come into popularity since then, but you can’t fix the old functions.
- vbezhenar 4y agoSo one should focus on creating versioned API IMO. Nobody does that for some reason. I should be able to declare my program to use v2023 API and consume libraries which use v1975 API.
- eadler 4y agoRust kind of does this. Language feature uses "edition" and has crates for its standard api instead of built in.
- tialaramex 4y agoIt really doesn't, though, although in principle Editions could provide an escape hatch it would be so drastic as to be largely unthinkable. Because of Rust's commitment to long term stability all of the standard library is there forever, even if it's a mistake and thus deprecated. std::u8::MAX will be in the library forever, even though you can write u8::MAX to get the same constant, name.trim_left_matches(remove) will exist forever even though you ought to write name.trim_start_matches(remove) because "left" assumes an LTR writing system. If NonZeroI64 is a bad idea, too bad it's in the standard library forever. If AddAssign is a bad idea, too bad it's in the standard library forever. The language syntax is allowed to evolve via Editions, but the standard library never breaks backward compatibility.
- actionfromafar 4y agoHey, don't get me started on semantics... what if you trim from the left or right, but are using RTL. Imagine your surprise when the exact opposite happens.
- masklinn 4y agoWhich is exactly why, as GP noted, rust deprecated trim_left/trim_right and added trim_start/trim_end.
- nicoburns 4y ago> It really doesn't, though, although in principle Editions could provide an escape hatch it would be so drastic as to be largely unthinkable Editions don't currently work for the std library, but I don't see why doing so would be drastic or unthinkable.
- tialaramex 4y agoWhereas C++ versions are each distinct languages which aren't necessarily interoperable, Rust Editions promise to interoperate and this is used all over the place. As a result the Editions can only "really" touch syntax, how you in some sense spell Rust, not what it means. Essentially Rust 2015 Edition and Rust 2021 Edition are actually exactly the same language except that the spelling is different, and as a result there are some things you can spell in one that you don't have a way to spell in the other. So in Rust 2015 Edition I can name my function async, because I want to, in Rust 2021 Edition that's fine, but it's written "r#async". Same function, same name in a sense, but different spelling. On the other hand, if I have an actual async function in Rust 2021 Edition, I can't write that in 2015 Edition at all - there's no way to spell the async keyword in that edition. The library however, is mostly semantics, not syntax. We care about what it does, not how to spell things on the whole.
- kelnos 4y agoBut the point is that there's no reason why editions couldn't touch semantics, too. There's no reason why they couldn't add a new attribute #[available_in_editions(2018, 2021)] that could allow them to actually remove deprecated functions, structs, trait, macros, whatever in newer editions. Code written for edition X would still compile and work, but code written for edition Y wouldn't be able to use things marked as unavailable.
- jmillikin 4y agoThat doesn't work in languages with nominal types, and even structurally-typed languages can have a hard time. You call a function that returns a v1975/SomeStruct and pass it to a function expecting a v2022/SomeStruct. What does the new function do when some of the struct fields are set to invalid values?
- netr0ute 4y agoWhat if the type was a "Some1975Struct" that got passed? Then that would give a huge blaring error because it's not the same type as a "Some2022Struct".
- jmillikin 4y agoThe naming convention isn't important. If the structs are from different versions of the library then you either have a type error (nominal types) or have to figure out the semantics of arbitrarily uninitialized fields.
- admax88qqq 4y agoYou do option 3, manually adapt one type to the other because they are different types. If you call one function that returns an int and need to pass it to another function that expects a char* what do you do?
- jmillikin 4y agoI think you may have lost track of what this thread is discussing. To be more concrete, let's say you have the following code (pseudo-Go): package main import "example.com/http_server" // v2 import "example.com/utils" // transitively depends on http_server v1 function main() { var srv *http_server.Server = http_server.New() utils.ServeHttp(srv) } You're going to have a problem, because ServeHttp expects an HTTP server struct from the v1 API, and you're passing in a struct from the v2 API. If you tried to write an adapter for this then (1) you'd need to somehow construct a concrete proxy of a type you don't control, and (2) there's no way to import both the v1 and v2 APIs at the same time in your code, so you don't have a way to declare a conversion function. That's why APIs with versions declared by dependency edges are impractical.
- yxhuvud 4y agoNo, one should focus on creating smaller independent modules and don't mix unrelated stuff like networking fundamentals with string convenience functions.
- beebmam 4y agoAre you familiar with linker hell? I agree in theory with your suggestion, but not for C or C-like languages which have such a poor story for linking compilation units.
- dietr1ch 4y agoWell, the problem is not getting easier and having poor versioning has been hunting everyone for around as long as null references
- kstenerud 4y agoWell, that and also a showcase of just how BAD the design of the C stdlib string functions was, and continued to be with every iteration where someone introduced a new bad-and-poorly-thought-out API to replace the old bad-and-poorly-thought-out API. It still boggles the mind how a function that leaves a C string potentially unterminated ever even made it into the standard...
- krelian 4y ago>It still boggles the mind how a function that leaves a C string potentially unterminated ever even made it into the standard... Easy to say with 50 years of hindsight. These are the pioneers we are talking about here. It boggles the mind how much they got right.
- kstenerud 4y agoWhile strcpy is forgivable considering it came from the primordial soup, there is no excuse for strncpy since it was designed to solve the problems with strcpy. And the string library is full of gaffes like this.
- tialaramex 4y agoNo, strncpy is not "designed to solve the problems". strncpy is a function to fill out fixed size data structures such as some on-disk data structures, with a variable length string it right pads the structure with null bytes. It does exactly what it was supposed to do, something that was pretty useful in 1970s UNIX programming but rarely if ever what you need today.
- icedchai 4y agoEvery teen hacker I knew had their own "my_strncpy" function that did something like strncpy(dst, src, len); dst[len-1] = 0;
- nicoburns 4y ago> Easy to say with 50 years of hindsight. These are the pioneers we are talking about here. Eh, well kinda. Languages with decent string handling predate C, so it's not like there wasn't precedent to follow. The creators of C were pioneers, but they were also people who favoured a quick, hacky approach over a clean careful one. That has certain advantages, but it's rather unfortunate that C has become so foundational and we've been stuck with those hacks.
- amelius 4y agoYou can make new functions while keeping the old functions, but making them print a warning that they are obsolete now.
- flohofwoe 4y ago> but libc needs to keep the same API The "needs to" is debatable. Compared to the C++ stdlib, the C stdlib is so small that adding a modernized and incompatible "v2" next to "v1" is realistic. The effort could start as a 3rd party implementation similar to MUSL. The old headers with the old APIs would still exist for "legacy code" but would generate "deprecated" warnings. Once that new "3rd party stdlib" has proven itself in the real world, the C commitee might consider it for inclusion in the standard.
- stephc_int13 4y agoWould you keep the naming style?
- flohofwoe 4y agoNo, at the very least I would add a (reserved) stdc_ prefix to all stdlib functions (and defines, and header filenames...), and keep options open for API versioning (e.g. stdc2_...). That way we could also get rid of all the random reserved identifiers we need to (theoretically) adhere to now.
- stephc_int13 4y agoThat's a good start, at least for function names, not sure if I would use the same logic for everything. In practice I find the prefix style better than namespaces, as a way to avoid information scattering. Snake case would probably win the race, but I am not sure that many old-timers would be ready to let go the old habit of abbreviated function names. After a while we don't see it for what it is, but I think that most of the naming is ugly, especially the string functions...
- jmillikin 4y agoDiscussion yesterday: https://news.ycombinator.com/item?id=34752400 https://news.ycombinator.com/item?id=34752400
- nn3 4y ago>Without libc you don’t have to use this global, hopefully thread-local, pseudo->variable. Good riddance. Return your errors, and use a struct if necessary. On contrary you should absolutely use errno, if only to report your IO errors. Or maybe he never does IO. After all a program that doesn't output anything is likely safer, since these are all evil side effects. I can just see that new programming paradigm: "side effect free programming" taking over the world in storm. Just based on that gaffe I'm not sure I can take anything else in that post seriously. It's clearly not based on any practical experience.
- AaronFriel 4y agoIt's not a gaffe, it's moving C in a direction that's safer and more like modern languages by removing mutable global state. That's what this means: "Return your errors, and use a struct if necessary."
- michaelhoffman 4y agoHe is saying that the design of errno is a flaw and that errors should be returned via other means. Not that you can safely ignore errno.
- aidenn0 4y agoLots of APIs allow reporting of I/O errors without use of an errno-like construct.
- wahern 4y ago> Lots of APIs allow reporting of I/O errors without use of an errno-like construct. libc included--the C11 threads API returns some errors directly using the constants thrd_busy, thrd_nomem, and thrd_timedout. Unfortunately, it also uses a catch-all constant, thrd_error, for other errors. The pthreads API returns errno values directly as return values, but that's POSIX, not C.
- manv1 4y agoIf you don't understand why errno is bad then it'll be hard to explain it. Errno is fine if you're accessing it right after a stdlib function that sets it, because it's the equivalent of a function's error code. Once you get out of that context errno becomes less and less useful and probably shouldn't be used, because without that context you won't know who/what actually set it.
- jeffrallen 4y agoWhen Go showed up on the scene, I was just about losing my patience with libc, and one if the things I found miraculous about Go was "no more strtok and friends".
- actionfromafar 4y agoI found it liberating to ignore strtok and friends even when coding in C :)
- heywhatupboys 4y agoif only "strsep" has existed? I mean just stop using strtok
- adrian17 4y ago> As with volatile, C is using the type system to indirectly achieve a goal. Types are not atomic, loads and stores are atomic I don't get this argument. This same wording can actually be used to make pointers untyped: "pointers are not inherently typed, loads and stores are typed. (In fact, LLVM IR recently made pointers untyped too)". And yet, clearly there's value in having them typed at the language level. Similarly here, there's clear value in having them part of the type system, if only to prevent a footgun of someone forgetting to mark a load as atomic/volatile.
- renox 4y agoIt's also very wrong for volatile: the compiler has to handle a volatile 'type' differently than regular types: you cannot 'cache' the value in a register, memory must be updated.
- tialaramex 4y agoThese type qualifiers cause the opposite foot gun. The programmer confidently writes compound assignments, which look like they're atomic/volatile thanks to the type qualifier but of course they are not. With intrinsics you don't have that problem. You just can't write the compound assignment - it doesn't exist, whereas in C it is just quietly compiled to two separate operations. C++ 20 deprecated this nonsense, but at almost the last opportunity WG21 voted to un-deprecate for C++ 23.
- adrian17 4y agoI'm not a big fan of restarting the discussion from the big Reddit argument on the same topic[1], but as I understand it: from embedded (or at least some developers') POV, this is a non-concern. Volatile never implied atomic in regards to interrupts, and pretending it should is wrong. On some platforms, single volatile load/store (whether `*ptr = 1234`, or `volatile_store(ptr, 1234)`) already can compile to "two separate operations" that can be interrupted in the middle. With that in mind, you are already supposed to be aware of that and execute such operations in no-interrupt contexts, and having this also apply to compound assignments is no more of a footgun than any other operation on such memory. (and if you do not need to care about the above for your platform/use-case, then I don't see why you would care about whether compound assignment compiles to one, two or more discrete opcodes) Not to mention, `REG |= 0x4` is just too entrenched (and seen as idiomatic) on some platforms. [1] https://www.reddit.com/r/cpp/comments/jswz3z/compound_assignment_to_volatile_must_be/ https://www.reddit.com/r/cpp/comments/jswz3z/compound_assign...
- secondcoming 4y agoI was unpleasantly surprised by how much locales are used for seemingly trivial things. It's even the case with the C++ iostreams stuff. I ended up writing my own functions in an 'ascii' namespace to avoid all this as much as possible. This [0] rant about locales and how they're nigh-on impossible to use correctly is quite amusing (slightly NSFW) [0] https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f027338b0fab0f5078971fbe https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f02...
- secondcoming 4y agoAs a follow up, if anyone uses `boost::lexical_cast` a lot, try defining BOOST_LEXICAL_CAST_ASSUME_C_LOCALE for some performance speed-ups and code size reductions
- Aanok 4y agoI understand that wm4 was probably a pretty difficult person to work with, but he got stuff done well and from the safe distance of my chair he was a rather entertaining fellow. The other mpv devs can't miss him but I kinda do.
- wheelerof4te 4y agoWhat are the alternatives to these libc functions? Making C even more bare bones than it already is makes no sense.
- sfpotter 4y agoHe explains in the article…
- pavlov 4y agoFor many things there are much better versions of the functions available in the operating system. For example on macOS you really should be using Core Foundation for strings, dates, etc. In a cross-platform application you can create a platform layer that wraps OS API usage and provides a POSIX fallback for the platforms you don't directly implement.
- marssaxman 4y agoIt's not as strange as you might think. Back in the classic Macintosh era (pre-OS X), it was quite common to write programs in C which essentially ignored libc. It was not a Unix system, so C's filesystem API made for an awkward fit, and the system's native callback-based IO was more efficient anyway. For similar reasons Mac programmers had little use for C's string functions or its allocator. Nor was there any terminal, so printf had little value. I never spent any time in the early DOS/Windows world, but from what I heard similar coding patterns were found there.
- icedchai 4y agoI learned C on the Amiga, and similar patterns were often followed. Example: we generally used the platform-specific AllocMem instead of malloc, even though a mostly complete C library implementation was available. One reason was AllocMem gave you finer control of what sort of memory was allocated (Amiga had "chip" memory, "fast" memory, and other oddities I now forget.)
- marssaxman 4y agoI wonder what a reasonably modern C library would look like, were we to design it from scratch?
- zellyn 4y agoThe last paragraph reads: > If you would like to see interesting innovation, check out what Cosmopolitan Libc is up to. It’s what I imagine C could be if it continued evolving along practical dimensions.
- naasking 4y agoI found that sentence weird because when I checked Cosmopolitan, it included most if not all of the functions this article was claiming shouldn't be used.
- marssaxman 4y agoThat confused me, too. Cosmopolitan is an interesting piece of engineering, but its goals seem entirely orthogonal to the topic of this article.
- KingLancelot 4y ago[dead]