10 ms·
My review of the C standard library in practice
- matheusmoreira 4y ago> In general, when working in C I avoid the standard library, libc, as much as possible. If possible I won’t even link it. Completely agree: freestanding C is a superior language. I'm so happy to discover I'm not alone in thinking like this. The author is very thorough in his criticism of libc, I learned a lot from the post. > The platform code is small in comparison: mostly unportable code, perhaps raw system calls, graphics functions, or even assembly. For me this is what made programming fun again! I don't even bother with multiplatform implementations anymore, I go straight for Linux system calls. Turns out to be a much better interface compared to libc. > On some platforms it will still link libc anyway because it’s got useful platform-specific features, or because it’s mandatory. If anyone would like to know why such a thing would be mandatory, I began writing about this subject literally just a few days ago. https://www.matheusmoreira.com/linux/system-calls https://www.matheusmoreira.com/linux/system-calls Still very much a work in progress but it does provide context for his claim.
- jcalvinowens 4y ago(Shamelessly plugging my own project) It's sort of amazing how little userspace code a crude webserver using Linux syscalls can be: https://github.com/jcalvinowens/asmhttpd https://github.com/jcalvinowens/asmhttpd One 4K page of code! No stack!
- stefanos82 4y agoAmazing job mate! +1 How much traffic can it handle, have you benchmark it?
- agumonkey 4y agohighly readable, kudos
- jmholla 4y agoThat's real neat. I was messing around with it an threw together this Dockerfile for a tiny server container. Just mount the desired folder to `/http`: FROM alpine as build RUN apk add --no-cache build-base gcompat nasm RUN mkdir /src COPY \* /src RUN cd /src \ && make FROM scratch COPY --from=build /src/asmhttpd /asmhttpd CMD ["/asmhttpd", "/http"] (Run: `podman build -t $IMAGE .; podman run -p 8080:80 -v $PATH:/http -d $IMAGE`)
- matheusmoreira 4y agoThat's awesome.
- drpixie 4y agoAgree. The vast majority of complaints about C are actually complaints about the standard library. It's library - if you don't like it, use something better :)
- eqvinox 4y ago> When using fread, some implementations use the entire buffer as a temporary work space, even if the returns a length less than the entire buffer. So the following won’t work reliably: char buf[N] = {0}; fread(buf, N-1, 1, f); puts(buf); This doesn't work reliably because the arguments are wrong. It's "fread(buf, size_of_item, number_of_items, file_ptr)". You're telling it to read one item of size N-1. If there's not enough data to read for a complete item, the rest of the "item" might be written with garbage. If you do it the right way around - reading N-1 items of size 1, you actually get the result you expect. Is this an unnecessary footgun in the standard C library API? Probably yes. But if you complain about it, I really rather you complain about the actual problem. Other than this, there's a few things I would agree with, and then a few other things that are issues with _specific_ standard C libraries, e.g. assert just exiting the process. Mixing C library API / POSIX problems with specific C library problems isn't particularly great for an article like this. Anyway, the article registers appropriately "uncooked" for someone reinventing pkg-config; as far as my personal "reputation database" is concerned this is strike 2 for the author.
- jdefr89 4y agoI too am surprised. The author generally writes clean code, most of it seems to rely heavily on libC… I am confused…
- eqvinox 4y agoThe article is indeed rather fuzzy about what "libc" is. It seems to exclude "things provided by the compiler" while talking about varargs, but later calls out atomics with no such note.
- arka2147483647 4y agoThe subject is messy. For example; traditionally memset() and memcpy() are clib functions, but ofcourse nowdays any good compiler will intrisic or optimize those. Same for atomics.
- deleted 4y ago
- stephc_int13 4y agoI also fully agree with the author and I am usually following a similar practice. The libc is the weakest point of the C programming language, outdated, bad naming conventions, and in some cases harmful APIs. It also has its merits, simplicity and availability, it is good as a fallback, but I think it is generally preferable to have a full API coverage when using a framework, including basic things such as printf/malloc/fopen as there are better ways to make those and it is quite important to maintain similar convention across the whole codebase.
- jdefr89 4y agoI am confused. Having gone over quite a bit of the authors C code on GitHub, it seems he uses libC quite often. I read/follow his blog posts; most are well written and insightful. His code is rather elegant as well, making his code bases good for studying (I recommend them to mentees)… What am I missing here. I think libc is quite alright given its time in operation, I bet libC is the most widely used standard library on the planet by a large margin. If not libC then what? Musl, glib? Those are great as well but I think avoiding libC is somewhat silly..
- flohofwoe 4y agoYou use your own or someone else's non-standard wrapper libraries for specific OS services. For instance if you want to do asynchronous IO, or use virtual memory features, the C standard library is entirely useless anyway, so you create your own thin cross-platform wrappers over OS specific system API calls, or even call those OS specific functions directly (e.g. POSIX functions on UNIX-flavoured operating systems or Win32 functions on Windows). The C stdlib is essentially an SDK for very simple 70's UNIX-style command line tools, but operating systems have moved on, while the C standard library is unfortunately stuck in the past. (IMHO a "C stdlib v2" would be much more important than any new language features, but it doesn't look like the C committee wants to go down that road).
- woodruffw 4y agoThe post doesn't say he doesn't use libc; it says that he tries to minimize his use of it. It's very difficult to entirely avoid libc. musl and glibc are implementations of libc, not alternatives to it.
- eqvinox 4y agoglib != glibc; glib is Gtk/GNOME's OS-independent wrapping layer and in fact an alternative to using libc functions directly. E.g. here is glib's take on strcmp() as entry point: https://docs.gtk.org/glib/func.strcmp0.html https://docs.gtk.org/glib/func.strcmp0.html
- 4y ago
- gavinhoward 4y agoWhile I'm not entirely sure some of his stuff works correctly, I find that I'm also a fan of getting rid of libc in my code. I already wrote a memcpy, and I already implemented my own buffered I/O. I might as well eliminate everything else that I can. setjmp and longjmp will have to stay, though.
- stephc_int13 4y agosetjmp and longjmp are very low-level and kind of borderline use-case. I practically almost never use those.
- gavinhoward 4y agoAnd that makes sense. For me, I have a stack allocator that knows about cleaning up, as well as about setjmp and longjmp, so I have a function I can call that will deallocate everything in the stack allocator and do the jump. It's basically a safe exception because everything will be cleaned up properly (if I allocate everything on the stack allocator or be rooted in the stack allocator, which I do).
- marssaxman 4y agoIs this allocator by any chance available online somewhere? I'd like to read and possibly use it.
- gavinhoward 4y agoIt's currently under three licenses to make it as radioactive as possible while the software it supports is under development. Once I'm done, however, it's going to be under a single copyleft license because I never meant it to be incorporated into any code other than my monorepo. It also doesn't even use malloc(), so you wouldn't care about a lot of the details. Links to it are in my comments, but I wouldn't look at it. Instead, I'll describe something you can implement with helps that would be much simpler. Keep a stack of data about allocations. You should keep a pointer to the allocation, the size, and the "destructor" (a function pointer to a function that takes a void pointer and returns nothing). Write 3 destructors. Two should do nothing, but they should be separate. One should be for scopes, and the other should be for functions. The last destructor should cast its void pointer to a jmp_buf pointer and do a longjmp() on it. Then have a normal allocation and deallocation function on the allocator. This is for normal allocations. Have a function to enter a scope, enter a function, and set a jump. In each case, they should push data onto the stack with the appropriate destructor. (I push a pointer to the function name for functions, for debugging.) Then have functions to exit a scope, exit a function, and do a jump. These should "unwind" all allocations until they reach an allocation that uses the scope destructor, function destructor, and setjmp() destructor, respectively. (You can text if two function pointers are equal to each other.) I'll be happy to answer any more questions.
- anthomtb 4y agoThe author rambled about the annoyances of string.h functions. And I do not disagree with many of the points made. Particularly strtoks global state and wholly unintuitive usage. But what is the alternative? Rolling your own equivalents?
- pavlov 4y agoMy approach is to use OS-specific C libraries like Apple’s CoreFoundation wherever possible. CFString is greatly superior to bad old C strings. If you need cross-platform, depending on your application it may be reasonable to create wrappers for your string usage with platform-specific implementations that use the best option available on each OS and fall back to POSIX.
- kevin_thibedeau 4y agoStrtok_r() is usually available as is the case for the other lib functions with global state. If not, it is a simple function to clone with whatever improvements you wish. I have a version that doesn't insert NULs so it can be used on read-only strings.
- Gibbon1 4y agoReminds me I need to rework mine to use slices.
- mqus 4y agoparticularly strtok is easy to replace imho. Ans if you've done so before, you can just accumulate and use your own "libc". Maybe not an entirely reusable one but enough of it. I can entirely imagine "rolling your own", as libc is not that big. But then again, I'm not a C dev by trade.
- randomNumber7 4y agoThe focus on setjmp/longjmp is strange. In my book you should have _very_ good reasons to use that. Of course you want it in there in case you need it, but writing so much about them when you say "standard library in practice" is a bit suspect.
- eschneider 4y agoThere are a lot of (ok, several :) very good reasons to use setjump/longjump in commercial applications and when you need it, you NEED it.
- deleted 4y ago[deleted]
- quelsolaar 4y agoI agree with lots of points in this article, but one advantage libc has is that many compilers implement intrinsic versions of memcpy, sin, sqrt and so on that will be faster than anything you can conceivably implement yourself in C. Still, the smaller dependency footprint you have the better, and that includes libc.
- megous 4y agoThese will/can be used even if you don't use libc.
- schemescape 4y agoBut wouldn’t that require splitting your implementation per platform? I’d rather leave that to someone else.
- badsectoracula 4y agoIf you avoid libc you'd need to do that anyway, otherwise how you'd open files, allocate memory, etc?
- schemescape 4y agoSorry, I meant as opposed to using some other library.
- Someone 4y agoMore or less. I don’t think declaring your own memcpy ‘inline’ will get you all the benefits of using the ‘real’ one. If you want your compiler to know what memcpy does, so that, for example, it can compile it as an inlined “load register, store register” instruction pair _if_ it knows it’s copying 8 bytes, you have to #include<string.h> (with the angle brackets; #include "string.h" won’t do) So, if you want optimal performance, you can’t do without the compiler header. You’re still free to link with your own implementation of memcpy, of course, but don’t expect it to be called in all places where the source code contains calls to it.
- forrestthewoods 4y agolibc is an absolute dumpster fire. He mentions the problems caused by the criminal use of globals in locales. That problem has lead to numerous blog posts that have hit the HN front page. This one comes to mind: https://aras-p.info/blog/2022/02/25/Curious-lack-of-sprintf-scaling/ https://aras-p.info/blog/2022/02/25/Curious-lack-of-sprintf-...
- dcreager 4y agoFor those who haven't seen it, CCAN [1] is a great collection of reusable C code, much of which specifically exists to work around the kinds of issues mentioned in OP. It turns “oh crap I should probably roll my own” into “wait I bet someone has already re-rolled this”. [1] https://ccodearchive.net/ https://ccodearchive.net/
- simplotek 4y ago> reusable C code, What's the rationale of providing this as a random set of code snippets instead of putting together a library?
- zeroonetwothree 4y agoNo expectation of support?
- simplotek 4y agoThere is zero expectation of support in FLOSS projects. You get what you paid form
- ddulaney 4y agoI really think it's more complicated than that. For sure, there's no legal expectation of support. If you don't support your open-source project, I can't sue you. But there is a social expectation of support. If the issues page is a ghost town, proposed patches just sit, and there hasn't been a release in years, we call the project "unmaintained" and discourage its use. At that point, if there's interest, somebody can fork it and support the fork, but forking is a serious act: it strongly signals that you don't like the direction that the current maintainer is heading. If that project is truly unmaintained, that's fine, but all-too-many open-source projects end up with rival forks when the original maintainer comes back. So yeah. There's zero legal expectation of support. But the social expectations are real, and providing a snippet (at least in my mind) has less of an expectation than a library.
- 4y ago
- WalterBright 4y agoI started the article thinking it's suspect. But it's very good, and worth reading for any C programmer.
- schemescape 4y agoWhat are some good, cross-platform, permissively licensed alternatives? I also mostly try to avoid the C standard library because I can’t be bothered to remember which functions are flawed and which are (mostly) safe to use. But I really don’t like to write my own platform-specific wrappers for e.g. converting between string encodings.
- asveikau 4y agoI agree with a bunch of points here, but this author reveals a spectacular gap when they mention qsort and bsearch without mentioning the high cost of the repeat function pointer calls on a modern CPU. A good qsort or bsearch mechanism would allow the comparisons to be inlined. Performance critical code can't use the libc versions due to this.
- jstimpfle 4y agoHave you measured? These functions can still come in handy at times (and inlining increase the binary size), and if your bottleneck is in a sorting routine, you likely have other problems. Quicksort would probably be the wrong choice anyway.
- asveikau 4y agoI 100% agree with the idea that the bottleneck might be somewhere else and it probably doesn't matter. But yes, I have seen numbers comparing qsort and std::sort. There is no competition in this. The latter is pretty much always vastly better in wall clock time due to inlining. Yes you trade code size for that, but today memory and storage is cheap.
- camel-cdr 4y agoThis isn't an inherit problem with libc, but rather a problem with the libc implementations. IMO libc's should offer an extern inline definition of those functions, so the compilers are able to inline or function specialize them when needed.
- asveikau 4y agoI think that's a good idea. I remember the days when an optimizer didn't really optimize a function pointer call. Now they can probably pull it off if it's the same compilation unit. And if you have link time optimizations, I've seen it do inlines across object files, but obviously that won't work for dynamically linked libc. But a static inline qsort in a libc header is probably a good call.
- afc 4y ago> A null pointer legitimately and usefully points to a zero-sized object. I agree with many points from the article but strongly disagree with this one. It makes much more sense to treat a null pointer as an invalid, not present, object. Treating null pointer as an empty string (perhaps by expecting them first N pages to contain just \0 bytes, as various older Unix systems predating Linux did) feels like a recipe to hide bugs. For example, I want to distinguish the output from getenv of "the variable was found but has an empty value" from "the variable was not defined". If you have some random struct Foo, it's very clear that a null Foo pointer just means "no valid Foo value" and attempting to dereference it should crash (rather than optimistically return ~random data and naively continuing program execution, making bugs harder to detect). I expect any reasonable programmer would suggest treating a string (or array of any types) identically.
- mgaunard 4y agoIn C++, std::string_view() refers to an empty string, with a null pointer and size of 0.
- rand_flip_bit 4y agoSadly, this actually isn’t true, and can cause some dangling/use-after-free issues. An empty string_view has a length of zero, but it’s representation might contain a null/non-null pointer + zero or two pointers that “compare equal”. I put compare equal in quotes because pedantically, after delete/free are called, they will not be required to compare equal.
- mgaunard 4y agoSee https://en.cppreference.com/w/cpp/string/basic_string_view/basic_string_view https://en.cppreference.com/w/cpp/string/basic_string_view/b... overload 1. Also it's "its", not "it's".
- dezgeg 4y ago> Treating null pointer as an empty string (perhaps by expecting them first N pages to contain just \0 bytes, as various older Unix systems predating Linux did) feels like a recipe to hide bugs. Empty string is not a 'zero-sized object'. It's an object with size of one byte that is zero.
- deleted 4y ago[deleted]
- jhallenworld 4y agoNo mention of printf and scanf. Printf is annoying because of the PRIu32 macros in case you want to print uint32_t in a portable way. How about conversion specifiers for the stdints.. %u32 %d32 or whatever. As for scanf: I want it to either succeed in parsing all of the arguments, or fail, but in this case leave the file position unchanged (so random access is required on input). This would allow trying to parse with another call to scanf, but with a different format string. For lists involving an arbitrary number of items, provide a conversion specifier that leaves the input alone and returns success: this way you can iterate the list. For example, these would be equivalent: if (scanf(f, "%d %d %d", &a, &b, &c)) printf("Success!\n"); if (scanf(f, "%d %e", &a) && scanf(f, "%d %e", &b) && scanf(f, "%d", &c)) printf("Success!\n"); /* %e means more input expected, without it scanf fails if no EOF at that point */ I should be able to provide my own parsing functions in it: imagine a conversion specifier for JSON that returns a tree. The existence of sprintf and sscanf point to bad design: I should be able to open a block of memory as a FILE, which would make sprintf and sscanf redundant. Also I should be able to open some kind of block device: meaning the FILE has pointers to user provided read and write functions. With this I could printf and scanf to some indirect device such as an EEPROM. We should be using scanf for the program's argument list. I mean the program should not get an array of pointers, but instead a FILE that contains the arguments. The memory layout is then abstracted. The pre-conversion of the command line to an array is totally unnecessary, only exists because libc's scanf sucks. Arbitrary length strings could have been handled with FILEs..
- leni536 4y agoFrom the article: > As for formatted input, don’t ever bother with scanf. With an entire other article about scanf as the link. edit: As for your other point, files may not even be seekable, let alone random access. How should that be implemented for a pipe?
- jhallenworld 4y agoAck, you are right, I read it too fast (triggered my pet peeve too early..) If the input is really not random access capable, provide buffering. You could imagine a version of FILE that automatically does this. There could be a some kind of truncate call to make it forget all past input, once you are sure it's not needed. In an OS you will have the resources to do this (malloc..). In other contexts (embedded), you may not.. but likely in those cases you don't have pipes. With a bit more we can have full arbitrary backtracking parsing. This parses "<hex> bob | <decimal> fred" or prints the unknown input: if( fscan_push(f) && fscanf(f, "%d %e", &n) && fscanf(f, "fred") && fscan_done(f) || fscan_backup(f)) printf("got fred(%d)\n", n); else if( fscan_push(f) && fscanf(f, "%x bob", &n) && fscan_done(f) || fscan_backup(f)) printf("got bob(%x)\n", n); else printf("got something else '%f'\n", f); '%e' expect more input fscan_push: save current location on a stack associated with FILE. Return true. fscan_done: drop location from stack. Return true. If stack becomes empty, drop input history up to current file position. fscan_backup: restore to location from stack. Return false. '%f' in printf prints the rest of the specified FILE on the stdout.
- jwilk 4y ago> the typical implementation doesn’t have the courtesy to trap in the macro itself What does it mean?
- kps 4y agoSome implementations have assert() call a function and trap inside that function. For example, glibc has assert() call __assert_fail(). That means that if you're looking at execution or a dump in a debugger, you're left at least one stack frame down from the actual failure.
- flykespice 4y agoThe thing that impressed me the most was the reserved identifiers rule in C, I didn't even know that existed and wow it's worse than I though, it's so ill-thought-out that it covers most generic words you can use in your code, I wouldn't be surprised if >90% of the existing C codebase out there is already violating it unaware. But just like most pitfalls in the language, it's a consequence of the early developers not putting much thought when assigning "namespaces" on the C library functions/macros. As for the locales non-standard behavior in GNU tools, it's no strange that GNU project always historically deviated from POSIX standard.
- nitwit005 4y agoHindsight is 20-20, but I do wish the c standard library had been a bit split up more. It would be nice to be able to say "I just want system calls and memory management". A lot of effort seems to go into stopping people from using some of the less well designed parts.
- WirelessGigabit 4y agoI remember reading another interesting article about the widening rules in C / C++ and one of the recommendations was that you could always widen when comparing small apples to medium apples. You'd both widen them to large apples. But not for the 128 one. There is no larger one. If anybody finds the article... I think it's applicable here on the xisdigit example.