7 ms·
Rethinking the C Time API
- tempodox 2y agoThe author could use a lesson in visual design. https://www.contrastrebellion.com https://www.contrastrebellion.com
- grg0 2y agoGood resource. The way I fix bad websites is by telling Firefox not to load external fonts and font styles.
- turtleyacht 2y agoUpdated link to referenced work Time, Clock, and Calendar Programming in C: http://www.catb.org/esr/time-programming/ http://www.catb.org/esr/time-programming/
- oliverkwebb 2y agoThanks, the link esr provides in his website (https://www.catb.org/~esr/faqs/time-programming/index.html https://www.catb.org/~esr/faqs/time-programming/index.html) leads to a 404
- mjburgess 2y ago95% of the supposed issues with C could be solved by a new standard library, integrating the debugger into the compiler as the default build/run environment (with auto address sanitisation, frame protection, etc. etc.), and a default strict mode error checking. It would then be actually really hard to successfully run a C program (in the debugger) with any problems. Under these conditions it'd be easy to imagine most C programs running with fewer bugs (, leaks, etc.) than Rust programs.
- pjmlp 2y agoExcept it is easier to introduce a new programming language than having a committee driven language to adopt a new standard library. Neither ISO nor OpenGroup would care about it. Remember that since 1989, no actions were taken to improve its security. Even the few functions that have been added still use pointer/length pairs without any means to validate they are the correct pair.
- Someone 2y ago> Remember that since 1989, no actions were taken to improve its security. Not much, but not nothing, either. gets was deprecated in C99 and removed in C11 (https://en.wikipedia.org/wiki/C_file_input/output#gets https://en.wikipedia.org/wiki/C_file_input/output#gets)
- pjmlp 2y agoYet the scanf and fgets possible exploits are still there.
- rdpintqogeogsaa 2y ago> Remember that since 1989, no actions were taken to improve its security. Technically, gets() was removed from the standard library in C11[0]. However, that is far from a semantically meaningful overhaul of the standard library. I nonetheless felt the need to point out that there was a very specific effort for the sake of completeness. [0] https://en.cppreference.com/w/c/io/gets https://en.cppreference.com/w/c/io/gets
- pjmlp 2y agoWhich is great, except for all those stubborn folks not using anything beyond C99, and scanf and fgets are still possible attack vectors, when getting sizes wrong.
- ctoshiningc 2y agoHave you tried talking to them?
- Keyframe 2y agomight as well throw in MISRA-C checker into it.
- oliverkwebb 2y agoThe core of C is pointer arithmetic; This creates a fundamentally unsafe environment. You can't even do anything in C without some asm (syscall wrappers) because C was meant to boil down and streamline PDP-11 assembly (Your computer is not a fast PDP-11) to a set of consistent principles. The consequence of this is that the core of the language is pointers and pointer arithmetic, and raw unabstracted pointers are fundamentally unsafe to work with. Using the rust type system I can essentially confirm code is bug and edge-case free with exhaustive matching and unit testing (C's lack of tooling blessed the world with autoconf and cmake btw). Not to mention rusts ability to abstract away necessary boilerplate gives me more time to think about my code instead of pointer arithmetic and allocation heuristics. The "cure" for C is a language that abstracts away raw pointers and memory allocation.
- oguz-ismail 2y ago> You can't even do anything in C without some asm (syscall wrappers) Which language can do anything without some asm and support as many platforms as C?
- oliverkwebb 2y ago> Which language can do anything without some asm and support as many platforms as C? I wouldn't be surprised if someone figured out how to do the interrupts and register control necessary to invoke syscalls with pure LISP/Scheme/CL :P P.S. anything that compiles with LLVM and has an ingrained way to do print() that doesn't invoke libc, although there's a blurry line here between "pure asm"/"compiles to asm" that involves trusting-trust-style bootstrapping of features into the compiler
- oguz-ismail 2y agoYou don't know what you're talking about
- nolist_policy 2y ago> > Which language can do anything without some asm and support as many platforms as C? > I wouldn't be surprised if someone figured out how to do the interrupts and register control necessary to invoke syscalls with pure LISP/Scheme/CL :P Haha > P.S. anything that compiles with LLVM and has an ingrained way to do print() that doesn't invoke libc, although there's a blurry line here between "pure asm"/"compiles to asm" that involves trusting-trust-style bootstrapping of features into the compiler Actually LLVM IR has no concept of syscalls, you have to use inline assembly inside your IR to issue syscalls.
- ctoshiningc 2y agoThe Rust Evangelism Strike Force clearly hasn't been defunded by DOGE (yet). Respectfully, if you want to use Rust, just use Rust. If C doesn't suit you, you don't have to use it, and you don't have to make unreasonable demands of the standards body.
- josephg 2y agoI think this is a really good idea, and zig shows how it could work. But this? > Under these conditions it'd be easy to imagine most C programs running with fewer bugs (, leaks, etc.) than Rust programs. This is a crazy goal. You will never out-rust rust by adding a few runtime checks to C, while in debug mode. Fewer bugs than rust code is a wild goal. I don’t think you understand just how much rust’s design prevents you from shipping bugs. It’s due to a combination of so, so many things. Like: references instead of pointers, unsafe blocks, sum types & match instead of unions, no implicit nullability, unwrapping optional values is explicit, the result type and #[must_use], bounds checks, the borrow checker preventing use after free, ownership semantics, Send & Sync for thread safety, and I’m sure plenty more. It’s common to write very complex, threaded rust code and have it work first time. Well, the first time it compiles. Coming from C, it’s wild. Or, really, just about any other language. To get the same result in C wouldn’t just need a “strict mode”. You would need to ban raw pointers - which would make it no longer C. And you’d need to make functions return more than an (easily ignored) status code. Ie, you want a result type. For bounds checking, you’d need a language level data structure for slices / arrays (pointer + length). You’d have to do away with void pointers for “generic” parameters. And probably 100 other tiny, breaking changes that the C community will never accept. And for all that, you would essentially get zig. Zig does all these things. But that would still get you worse bug density than rust because you don’t have a borrow checker. It’ll get you close - Runtime checks in debug mode will detect your use after frees - if you have a good test suite. But they won’t prevent aliasing. Or (I think) help with thread safety. For that, you need a borrow checker. You need rust.
- mjburgess 2y agoWe're talking about different kinds of bugs. One claim i'm making here is the poor iteration time imposed by Rust's complexity, significant design constraints, and so on, themselves cause kinds of bugs which are more readily addressed in C. Eg., I think memory leaks are easier in std Rust than in the kind of C i'm talking about. I don't think we have a good evidential basis for comparing the total class of programming bugs in Rust vs. comparable langs -- since there isn't that much Rust code. One "empirically ambitious" claim here is that the very high complexity of rust isn't design-bug-free, and "getting to 95%" with a modern C toolchain retains a very low-complexity get-it-done-and-iterate style of programming which has many "bug free'ing" advantages. Esp. if supported by a "debugger-oriented std lib"
- shakna 2y ago> strftime() has to write to a string of a fixed length it can not dynamically allocate (This is less legacy than it is bad design) There's good reason for this. I disagree that it's a bad design. strftime can legitimately produce zero-length strings, in a non-error state. You do not want an allocation on the heap, that is empty. You'd end up with more error states to track, and more confusion around whether the function had succeeded. (Especially when using %c).
- CamperBob2 2y agoA zero-length C string is still one byte long, for better or for worse.
- shakna 2y agoRight. So you have an extra byte you need to free. One that you can't introspect, and reading will cause a memory fault, because it won't be NULL-terminated (0-length means 0 length). And not freeing, because the assumption of 0-length is violated, leads to a memory leak. So instead of just checking one return value, now you have to check two. And people are not great at even handling a single NULL check. Few people check malloc's return, as awful as that is. Design should be intuitive as possible. You can't assume they'll even look at a manpage. If something returns a length, then people assume that length is what will be allocated. A valid 0-length time string, violates that assumption, and will cause problems down the line. If someone is forced to do the allocation themselves, then there's a greater chance they'll actually notice that they need to free it.
- CamperBob2 2y agoWhat? No, the null terminator is the single byte in question. That's how an empty string is represented in C. It's not the same thing as a NULL pointer, as you may be thinking.
- mastax 2y agoI wasn’t aware that on non-x86 platforms long double is often implemented with quadruple precision. I had assumed it was an x87-specific hack. On ARM64 windows/macos long double is apparently 64-bits which could be a problem. Personally something about that solution is unsatisfying. Feels like it’d be slow, even though that wouldn’t matter 95% of the time. I’d rather have 128-bit integer of nanoseconds. https://en.wikipedia.org/wiki/Long_double https://en.wikipedia.org/wiki/Long_double
- immibis 2y agoThe requirements for time in computers have increased drastically since C was invented. Time used to mean what we write down or see on a clock: 2024-08-13 02:27 PM. The computer had an electronic clock built in, so it could save you a little effort of looking at the clock and copying the numbers. And that was all it did. If your clock was a few minutes off, that was no big deal. People knew clocks only agreed to within one or two minutes. People knew clocks were different in far away lands. Some people knew that you have to adjust your clocks twice per year. The computer clock was just like any other clock but happened to be inside a computer. Now we expect a globally synchronized unique identifier for each instant, regardless of timezone. This is hard to deliver. Computers use these for synchronization amongst themselves, so they have to be accurate to milliseconds or better. This is hard to deliver. We expect computers to handle requests from far away lands with as much grace as requests from the local operator, and deliver results in a format the people in those lands expect. This is hard to deliver. We expect computers to process requests about the past using information that was current in the past, all over the world. This is hard to deliver. We expect computers to automatically adjust their own clocks twice a year, not just on the dates everyone in your local area does, but for users in all parts of the world on their respective dates. This is hard to deliver. And we still haven't got graceful handling of completely different calendar systems.
- Gibbon1 2y agoInteresting thing about hardware real time clocks. They are usually as bad as the C time API. I worked with a guy that designed three RTC clock cards for an industrial bus system The first had registers for hours minutes, seconds, day, month, year. He was proud it handed the leap years correctly. It had a battery back up. The second design just had a 48 bit counter that counted ticks. You could read and and write it using latches so the read/writes were atomic. The third design was a read only 48 bit counter and a 1024 bit battery back ram. In the second and third design the conversion from time to a string is done in software. In the third the clock just provides a free running timer and you store an offset in battery backed ram along with other time related meta data. The vast majority of hardware RTC clocks today are implemented like the first example. It's a little infuriating since my coworker figured out how to do it right 45 years ago.
- ajross 2y ago> Out of all the components of C, its time API is probably the one most plagued with legacy cruft. First off, no, locales and wide characters exist. This statement is just laughable. But even as to time: that seems really unfair. This whole area is a footgun and has been the source of bad implementation after bad implementation, in basically every environment. But among those: The "struct tm" interface is notable for being: 1. Very early, arriving in C89 and the first drafts of POSIX, with working implementations back into the mid 80's. 2. Complete and correct, able to give correct calendar information for arbitrary named time zones in an extensible and maintainable way. LOTS of other attempts got stuff like this wrong. 3. Relatively easy to use, with a straightfoward struct and a linear epoch value, with conversion functions in each direction, and only a few footguns (the mix of 0- and 1-indexing was unfortunate). There are even a few quality of life additions like support for "short" month names, etc... Really, these routines remain useful even today, especially since their use is guaranteed to integrate with your distro's time zone database which must be constantly updated to track legal changes. There's stuff to complain about, but... no, I think the premise of the article is dead wrong.
- chrsig 2y agoi don't read that as a condemnation of usefulness of integrating w/ the system tzdb (every language stdlib does this under the hood) my biggest issue is that the structs and function definitions are pretty opaquely named and make it very hard to learn let alone teach newcomers.
- ajross 2y ago> 1. Very early, arriving in C89 and the first drafts of POSIX, with working implementations back into the mid 80's. I looked it up. In fact it's much earlier than that. The API arrived in time.h via v7 Unix in 1979. And it remains, unchanged, pervasively used, and most importantly still used for new code, four and a half decades later. Rather than "legacy cruft", this constitutes one of the most successful utility APIs in human history.
- ctoshiningc 2y agoI mean, this blog post is the kind of uneducated posturing that makes the JavaScript kiddies happy, because "crufty, old C is so bad, see!?". But none of them know enough to know that it's basically all bullshit.
- npalli 2y agoAmong the many improvements, time is one area where C++ has become better than old school C cruft. In c++20/std::chrono, the Lua like code is just this - auto now = system_clock::now(); zoned_time local_time{current_zone(), now}; std::cout << std::format("{:%a %b %d %T}\n", local_time);
- burntsushi 2y agoOr Rust, with Jiff: println!("{}", jiff::Zoned::now().strftime("%a %b %d %T"));
- pjmlp 2y agoSome C++23 flavour, :) auto now = system_clock::now(); zoned_time local_time{current_zone(), now}; std::println("{:%a %b %d %T}", local_time);
- bloak 2y agoIn an article like this I would have liked to see some mention of TAI (https://en.wikipedia.org/wiki/International_Atomic_Time https://en.wikipedia.org/wiki/International_Atomic_Time) as one of the alternatives to UTC. Unfortunately there are several different universal times. Apparently there's also a "Galileo System Time", for example.
- asveikau 2y agoI fail to see TFA's concerns or take them very seriously. > time() unnecessarily takes a pointer argument to write to Minor cosmetic issue. > strftime() has to write to a string of a fixed length it can not dynamically allocate (This is less legacy than it is bad design) This is often a good way to structure string functions in C. The fact that TFA repeated the constant 40 instead of using sizeof() immediately signals that they are unfamiliar with the idioms. A "you problem". Doing heap allocation where it is not required could be a problem for some use cases. > localtime() needs the pointer to a time_t value even though it does not change it because of register size concerns on PDP-11’s Also minor and cosmetic. > sleep() cannot sleep for sub-second amounts of time, usleep() is deprecated and it’s alternative nanosleep() requires you to define variables sleep(3) is not really a "time function" in the sense of the others mentioned, it is a thread scheduler function. As such it kind of exists in a different universe. This is also shown by the fact that it's part of POSIX and not the C standard, like time(2) is.
- ctoshiningc 2y agoAnother classic case of "if everyone does something differently than you do, it might be worth investigating why". The hubris to think that basic C time functions have been "broken" all this time, and that nobody noticed or cared. What a joke.
- oliverkwebb 2y agoFor a definition of "nobody" that includes Eric S Raymond, one of the most prominent figures in the linux world who's article (https://www.catb.org/esr/time-programming/index.html https://www.catb.org/esr/time-programming/index.html) I reference multiple times. [plonk](https://www.catb.org/jargon/html/P/plonk.html https://www.catb.org/jargon/html/P/plonk.html)
- 1718627440 2y ago> Eric S Raymond [...] prominent figure[...] in the linux world Doesn't he work for Microsoft?
- Joker_vD 2y ago> keep in mind that Integers support One percision, and there’s a trade off between resolution and the bounds of your epoch, Floating point values support all percisions, there is no such trade off. Yeah, except with integers you get guaranteed precision across all of your data range while with floating point, it is ridiculously easy to accidentally lose precision without noticing it when e.g. shifting time deltas from the past into the future. Not to mention that using floating-point number of seconds since epoch means that the times around the epoch are always given better precision than the timestamp around the current time which is really not what you want, and the situation only worsens with time.
- Animats 2y agoTime parsing and formatting is prone to extended bikeshedding. I once raised the issue that Python had five parsers for ISO 8601 date formats, and they were all broken in some way. It took a decade to resolve that. By then I'd moved on to Rust.
- atiedebee 2y agoI don't think having strftime return a malloc'd pointer is a good idea. The string won't be large at all and can easily fit onto the stack (just like it was done in the example code). If I want to use a custom allocator to store the string, I can. If I want to malloc the string I can.
- IYasha 2y agoIndeed, when I wrote a C utility and just wanted to output its start, finish and run time, I spent MORE TIME THAN IT TOOK TO WRITE THE WHOLE PROGRAM to figure out how the whole date/time garbage works! This was a painful, maddening experience. As if this entire API was designed to drive you mad. But I still love C anyway.