6 ms·
Weekend projects: getting silly with C
- mgaunard 2y agoaren't the switch shenanigans important to the duff's device?
- tialaramex 2y agoDuff is relying on the fact you're allowed to intermingle the switch block and the loop in K&R C's syntax, the (common at the time but now generally frowned on or even prohibited in new languages) choice to drop-through cases if you don't explicitly break, and the related fact that C lets your loop jump back inside the switch. Duff is trying to optimise MMIO, you wouldn't do anything close to this today even in C, not least because your MMIO is no longer similarly fast to your CPU instruction pace and for non-trivial amounts of data you have DMA (which Duff's hardware did not). In a modern language you also wouldn't treat "MMIO" as just pointer indirection, to make this stay working in C they have kept adding hacks to the type system rather than say OK, apparently this is an intrinsic, we should bake it into the freestanding mode of the stdlib. Edited to add: For my money the successor to Tom Duff's "Device" is WUFFS' "iterate loops" mechanism where you may specify how to partially unroll N steps of the loop, promising that this has equivalent results to running the main loop body N times but potentially faster. This makes it really easy for vectorisation to see what you're trying to do, while still handling those annoying corner cases where M % N != 0 correctly because that's the job of the tool, not the human.
- uecker 2y agoNot sure what you mean by "hacks to the type system". All modern computing essentially converged to unified memory, which is exactly C's model.
- tialaramex 2y agoWhile it's convenient technically to have unified memory and so it makes a lot of sense for your machine code, in fact the MMIO isn't just memory, and so to make this work anyway in the C abstract machine they invented the "volatile" qualifier. (I assume you weren't involved back then?) This should be a suite of intrinsics. It's the same mistake as "register" storage, a layer violation, the actual mechanics bleeding through into the abstract machine and making an unholy mess. If you had intrinsics it's obvious where the platform specific behaviour lives. Can we "just" do unaligned 32-bit stores to MMIO? Can we "just" write one bit of a hardware register? It depends on your platform and so as an intrinsic it's obvious how to reflect this, whereas for a type qualifier we have no idea what the compiler did and the ISO document of course has to be vague to be inclusive of everybody.
- uecker 2y agoI wasn't involved back then, but I know the history. I thought you were talking about something more recent. But this is all opinions and terms such as "unholy mess" etc do not impress me. In my opinion "volatile" is just fine as is "register. Neither are layer violations nor a type system problem. That the exact semantics of a volatile access are implementation defined seem natural. How is this better with an intrinsic? What I would call a mess are the atomics intrinsics, which - despite being intrinsics - are entirely unsafe and dangerous and indeed mess (just saw a couple of new bugs in our bug tracker).
- tialaramex 2y agoSure, it's just an opinion. I think the consequences speak very well for themselves.
- uecker 2y agoWhat consequences?
- tialaramex 2y agoBecause MMIO is made to look like it's really just memory (rather than a technical convenience) C programmers use the MMIO the same way they would the heap memory in the abstract machine. Sometimes the compiler will correctly intuit what needs to actually be emitted, sometimes the hardware they're actually talking to will compensate for what actually happens - other times it just "misbehaves" because this is not memory and so it doesn't behave like memory.
- masklinn 2y ago> Duff is relying on the fact you're allowed to intermingle the switch block and the loop That's just a special case of being able to intermingle switch with arbitrary syntax, which is what TFA does, before it jumps to computed gotos.
- doe_eyes 2y agoThe overarching point appears to be getting rid of angle brackets, which is not something that Duff is doing. Further, Duff's device keeps case labels on the left of its control structure; moving ifs to the left is the other "innovation" here. I think you really have to squint your eyes to see the similarities, beyond the general theme of exploiting the counterintuitive properties of switch statements.
- mgaunard 2y agoTo me the duff's device is just a mechanism to unroll a loop without having to duplicate the code for the trailing case. While you can't use SIMD you can still benefit from instruction-level parallelism. It's potentially better in some scenarios where you want to minimize instruction cache usage and there are few iterations of the loop.
- smusamashah 2y agoFound these silly tricks by the author of this blog on twitter first. Switch statement can do loops too https://twitter.com/lcamtuf/status/1807129116980007037 https://twitter.com/lcamtuf/status/1807129116980007037
- viraptor 2y agoAlso on the actually social network https://infosec.exchange/@lcamtuf/112701486085621844 https://infosec.exchange/@lcamtuf/112701486085621844
- teo_zero 2y agoAnother source of surprise: 4[arr] // same as arr[4]
- stefanos82 2y agoThanks to array decay to pointer, we basically have `*(array_label+offset)` which in this case of yours we have `*(offset+array_label)`; in other words, `*(arr+4)` is the same as `*(4+arr)`...that's it, really!
- trealira 2y agoBy the same principle, these are exactly the same: arr[i][j] j[i[arr]] These are the simplifications you'd do. You only need to know that a[x][y] is equivalent to (a[x])[y], and that a[x] is the same as x[a]. arr[i][j] (arr[i])[j] (i[arr])[j] j[i[arr]]
- geon 2y agoThis can be used to implement coroutines in C. https://stackoverflow.com/questions/24202890/switch-based-coroutines https://stackoverflow.com/questions/24202890/switch-based-co...
- emmericp 2y agouIP (TCP/IP stack for tiny microcontrollers) is a another fun real-world example for these types of coroutines: https://github.com/adamdunkels/uip/blob/master/uip/lc-switch.h#L43 https://github.com/adamdunkels/uip/blob/master/uip/lc-switch...
- nxobject 2y agoIf only there was a way of using setjmp/longjmp-style contexts instead of goto, un/winding the stack as required. So we could travel around in time... unfortunately you can't work with a setjmp buffer before it's actually created, unlike gotos.
- gpderetta 2y agosigaltstack tricks to the rescue! (Although POSIX only, not ISO C)
- JohnMakin 2y agoMy undergrad was entirely in the C language and I’m very glad for it. Sometimes more modern languages can throw me for a loop, no pun intended, but the beauty (and horror) of C is that you are pretty close to the metal, it’s not very abstracted at all, and it allows you a lot of freedom (which is why it’s so foot gunny). I will never love anything as much as I love C, but C development jobs lie in really weird fields I’m not interested in, and I’m fairly certain I am not talented enough. I have seen C wizardry up close that I know I simply cannot do. However, one of the more useful exercises I ever did was implement basic things like a file system, command line utilities like ls/mkdir etc. Sometimes they are surprisingly complex, sometimes no. After you program in C for a while certain conventions meant to be extra careful kind of bubble up in languages in a way that seems weird to other people. for example I knew a guy that’d auto reject C PR’s if they didn’t use the syntax if (1==x) rather than if (x==1). The former will not compile if you accidentally use variable assignment instead of equality operator (which everyone has done at some point). This tendency bites me a lot in some programming cultures, people (ime) tend to find this style of programming as overly defensive.
- smackeyacky 2y agoIn an embedded environment, overly defensive is an asset
- JohnMakin 2y agoThat’s precisely where my little professional C experience was. I then switched to a python shop and was initially horrified at some conventions, took some getting used to.
- deleted 2y ago[deleted]
- uecker 2y agoI force my students to do C development. And it turns out that it is not that hard if you approach it with modern tools which catch a lot of problems. The lack of abstraction is fixed with good libraries. C evolved a lot and many foot guns are not a problem anymore. For example for if (x = 1) you nowaday get a warning. https://godbolt.org/z/79acPPro6 https://godbolt.org/z/79acPPro6 Implicit int, calling functions without prototypes, etc. are hard errors. And so on.
- JonChesterfield 2y agoThis features the construct switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; } which is a switch where you don't have to write break at the end of every clause. #define brkcase if (0) case That might be worth using. Compilers won't love the control flow but they'll probably delete it effectively.
- jppittma 2y agoI think it is super unclear how this works, and I would prefer the same control flow using goto, rather than the duffs device style switch abuses.
- leni536 2y agoSurely the following would work just as well? #define brkcase break;case kinda defeats the purpose of the macro even.
- MaxBarraclough 2y agoThat strikes me as better. The original macro presumably misbehaves if there's more than one statement in a sequence, as the if will only affect the first statement.
- wrsh07 2y agoI think the behavior is slightly different since this one breaks the above case, and the other one only omits its case from fallthrough Incidentally, what happens if you use your brkcase as the first case? I don't find either particularly exciting - a macro that would append break to the current case feels better
- leni536 2y agoBoth version of the macro makes this fall through from 0: switch (a) { brkcase 0: foo(); case 1: bar(); } so in a sense the `if (0) case` trick also affects the previous case, not the current one. But that one also falls apart when there are multiple statements under the brkcase.
- fanf2 2y agosee also https://www.chiark.greenend.org.uk/~sgtatham/mp/ https://www.chiark.greenend.org.uk/~sgtatham/mp/ Metaprogramming custom control structures in C by Simon Tatham
- metadat 2y agoDiscussed in July 2021 (43 comments): https://news.ycombinator.com/item?id=27781784 https://news.ycombinator.com/item?id=27781784
- quietbritishjim 2y ago> The above example will print the value of a, but it won’t be initialized to 123! It certainly could do though. In C, using an uninitialised variable does not mean "whatever that memory happened to have in it before" (although that is a potential result). Instead, it's undefined behaviour, so the compiler can do what it likes. For example, it could well unconditionally initialise that memory to 123. Alternatively, it could notice that the whole snippet has undefined behaviour so simply replace it with no instructions, so it doesn't print anything at all. It could even optimise away the return that presumably follows that code in a function, so it ends up crashing or doing something random. It could even optimise away the instructions before that snippet, if it can prove that they would only be executed if followed by undefined behaviour – essentially the undefined behaviour can travel back in time!
- uecker 2y agoUB can not travel back in time in C. Although it is true that it can affect previous instructions, but that code is reordered or transformed in complicated ways is true even without UB.
- emmericp 2y agoThe time-travelling UB interpretation was popularized by this blog post about 10 years ago [1]. I'm not enough of a specification lawyer to say that this is definitely true, but the reasoning and example given there seems sound to me. [1] https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633 https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63...
- uecker 2y agoYes, random blog posts did a lot of damage here. Also broken compilers [1]. Note that blog post is correct about C++ but incorrectly assumes this is true for C as well. [1]. https://developercommunity.visualstudio.com/t/Invalid-optimization-in-CC/10337428?q=muecker https://developercommunity.visualstudio.com/t/Invalid-optimi...
- jftuga 2y agoThis reminds me of some silly C code I once wrote for fun, which counts down from 10 to 1: #include <stdio.h> // compile & run: gcc -Wall countdown.c -o countdown && ./countdown int n = 10; int main(int argc, char *argv[]) { printf("%d\n", n) && --n && main(n, NULL); } Python version: import sys # run: python3 countdown.py 10 def main(n:int): sys.stdout.write(f"{n}\n") and n-1 and main(n-1) main(int(sys.argv[1])) Shell version: # run ./countdown.sh 10 echo $1 && (($1-1)) && $0 $(($1-1))
- cbrpnk 2y agoI don't think I've ever thought of explicitly calling main(). Made me chuckle.
- akdev1l 2y agoI think it is UB Edit: actually looks like it is UB in C++ but not C
- colejohnson66 2y agoWhy would calling main be UB!? How is crt0 supposed to work?
- tomjakubowski 2y agocrt0 generally isn't C and isn't subject to C's rules
- pdimitar 2y agoFun at parties alert: Let's stop getting silly with C, too many CVEs! --- Serious comment: It's a rather cool article actually. Not something I'd do daily but it's kind of sort of useful to know these techniques.
- nj5rq 2y agoWhy did I not know that this: case 1 ... 10: Is valid C? I have been programming in C for years, what standard is this from?
- G4E 2y agoUnless it has been recently standardized it's not valid C, it's a GNU extension.
- dekhn 2y agoIt appears to be a GNU C extension: https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html but I couldn't find the history of the extension. I believe it is not in standard C (not sure about clang).
- o11c 2y agoDue to the way lifetimes work in C (they begin with the block, not the declaration), the following is legal: #include <stdio.h> #include <stddef.h> int main() { { int *p = NULL; if (p) { what: printf("a = %d\n", *p); return 0; } int a = 123; p = &a; goto what; } }
- junon 2y ago> switch (i) case 1: puts("i = 1"); I've seen this in the wild, particularly with macros. #define assert(c) if (!c) ... if (foo) assert(...); else bar(); // oops!
- codext 2y agoThe final obfuscated code snippet in the article brought to light another GCC extension: https://stackoverflow.com/questions/34559705/ternary-conditional-operator-without-the-middle-expression https://stackoverflow.com/questions/34559705/ternary-conditi...
- deleted 2y ago[deleted]
- drzzhan 2y agoI am so lost at the final block of code. Does every C developer have to deal with this everyday?
- BenjiWiebe 2y agoNot even close. If any C developer ever has to deal with that ever, something somewhere went horribly wrong.
- ICameToComment 2y agoCertainly not. That's the purpose of the article where they say in the final sentence that it's entirely possible to write readable, yet totally befuddling code in C that stands a chance in the IOCCC.