4 ms·
It's problems like these (mistaking a sentinel value for a valid member of a type) that make me think sentinel values are just a mistake. In-band signalling mus
by dwattttt 2y ago
It's problems like these (mistaking a sentinel value for a valid member of a type) that make me think sentinel values are just a mistake. In-band signalling must have been responsible for so many bugs at this point.
- ajross 2y agoIt's hard though. I mean, it's easy to say you shouldn't do this. But that becomes isomorphic to "You shouldn't have NULL". And... that's hard, because you really need NULLs for a lot of problems. Or "You shouldn't have error code returns from syscalls", when you really need those. It's possible to split it out, but then everyone complains about the result. We thought exceptions were going to fix the error code problem, but then we collectively decided we hate them. OSes and static analyzers and instrumentation tools like valgrind/asan/msan have done a really good job of turning NULL dereference into a built-in assertion everywhere, but people won't use them. Even Rust, which has probably come closest to a genuine solution, has done so only at the cost of immense complexity and somewhat fugly syntax. No free lunch. I think what's notable here isn't the bug itself, which is pretty routine. It's the fact that simple bugs can be buried so deeply that they get exposed only in a six-hour-long test rig on one particular Linux distribution's backend.
- dwattttt 2y agoRust's answer is its type system, but Rust is hardly the only language that can do this. If a syscalls returns an fd, and on error it returns a negative value, why not define it as a bitfield with one bit a flag? Then in C there'd at least be a field name you could grep for, or grep for it not being there.
- AlotOfReading 2y agoLet's steelman this and ignore the portability issues C has with bitfields on ABIs. Many processors don't have a BFI instruction, so working with a bitfield will generate multiple instructions. At the scale of a large program, this can easily result in measurable code bloat and significant (single digit percent) performance overheads on the happy path. This is actually a discussion that's been going on in the systems programming community for decades. There's a cultural aversion to the runtime costs that's always outweighed any hygiene advantages.
- dwattttt 2y agoWhat's the difference between checking the highest bit is set in a struct's bitfield, and checking the highest bit is set when that bit is part of an integer?
- AlotOfReading 2y agoBack in the day, there also would have been one's complement systems where there was no sign bit and checking a negative value makes much more sense than "whatever the highest bit is" in that. Thankfully those are mostly dead and gone. The sign bit has much more defined semantics regardless. You can sign extend it. It's guaranteed to be the MSB, rather than dependent on how your compiler does struct layout and every ABI will agree on where it lives. You don't have to worry about a mismatch between the language semantics of the payload and how any particular ISA treats the "integer". Etc.
- dwattttt 2y ago> The sign bit has much more defined semantics regardless. But which of those semantics do we need for conveying which of two states something is in? If the Linux kernel recommended a more rich type for expressing the result of a syscall, compilers would find a way to express that portably. Or they wouldn't recommend it. Or compilers wouldn't make it portable. Or libc implementors won't use it. Or lots of other possibilities. And we keep living in the world where we kill a process (often init) when we forget to check a syscall result, and pass fd-or-error somewhere that takes an fd, because those things are both integers.
- paulddraper 2y ago> Even Rust, which has probably come closest to a genuine solution Haskell Maybe Scala Option Java Optional These things have existed for over a decade (multiple, in the case of Haskell).
- crest 2y agoAll of these didn't just give you something equivalent to C malloc()/free() with a parameter and result type. Maybe monads are useful in a lot of cases, but require a safe type system that prevents accidentally/negligently ignoring errors and language features to make it easy to unwrap the enum variants e.g. destructuring pattern matches and a collections of functions to work with them. I know both Rust and Haskell have those and suspect the other two examples as well. Without them they would be a pain in the ass to use.
- alpinisme 2y agoI think that’s the point though. This is an area where C just isn’t expressive enough and other languages have come in with a solution to fix that.
- ajross 2y agoThe question isn't the ability to handle errors, though. You can do it trivially in C too. It's whether or not the design chosen to permit that has benefits that outweigh its costs. And as I said I think the jury is very much still out on that one. Again, we tried to do this with exceptions in the early 90's and whiffed badly. Doing it at the validation/assertion/fuzzing/coverage layer with instrumentation tools turns out to be really effective in practice but the uptake remains really low. Today the mindscape points to Rust's Option as the saviour, which is why I listed it. But as you point out, all these other environments had it much earlier. And... kinda failed. So I'm not hopeful. In-band sentinels like NULLs and error codes are (1) clear and obvious, (2) near-zero-overhead and (3) extremely useful. Everything else falls down on at least one of those requirements.
- paulddraper 2y ago
- crest 2y agoThe answer is to make enums explicit e.g. malloc should return either a valid allocation or an error, but you can't easily do that in C. It takes a richer/better type system to make this convenient to use and safe from accidents. Depending on your language and compiler the in memory representation for a nullable and a non-nullable pointer can be the same, but a well designed type system can prevent fallible human programmers from confusing one for the other. Add some destructuring pattern matching to the language the usability improves instead of suffers. Rust added a whole lot more which made the syntax very noisy.
- kelnos 2y agoAt the risk of being that guy who always brings up rust: enum Process { Invalid, Terminated, Pid(libc::pid_t), } There would be no way you could pass instances of the first two enum variants to kill(). Rust is of course not the only language where you can express something like this; e.g. in Scala2: sealed trait Process object Process ( case object Invalid extends Process case object Terminated extends Process case class Pid(pid: Int) extends Process } Does D have algebraic data types? Only allowing enums to be represented by integers is a language weakness.