Duff's device
en.wikipedia.org
en.wikipedia.org
I learned about the theory with it, and used his library to implement cooperative multitasking on a bare metal embedded AVR C project. I wrote a task manager and dispatcher based on Protothreads, and that effort was what later made it simple for me to understand the JS async/await paradigm that would appear under my radar a couple years later.
EDIT to add that I think the concepts might still apply, since these days I've seen things like stackless async coroutines discussed for Rust... didn't study those in detail yet, but I wouldn't be surprised if the basic concepts are the same.
[1]: Current website: http://dunkels.com/adam/pt/
The source code is interesting. LC_SET is particularly neat:
/* WARNING! lc implementation using switch() does not work if an
LC_SET() is done within another switch() statement! */
/** \hideinitializer */
typedef unsigned short lc_t;
#define LC_INIT(s) s = 0;
#define LC_RESUME(s) switch(s) { case 0:
#define LC_SET(s) s = __LINE__; case __LINE__:
#define LC_END(s) } #define LC_SET(s,num) s = num; case num:
This will let you serialize the state s, modify the program, recompile and reload s, and have it resume at the desired point.Alas, this isn't a part of the standard (iirc), so you need to depend on compiler-specific features.
LC_SET(s,0)
A
+ LC_SET(s,2)
+ C
LC_SET(s,1)
B
When the number for each resume point is explicit instead of automatic, you can edit the source code while controling where any existing suspended coroutines will resume at.There is something I miss in modern languages...they do not permit the abuse that C permits. The preprocessor, pointer arithmetic, various semantics around types and overflows...one of course sees some horrendous monstrosities in the Linux kernel source...
C as a tool is only an approximation of the underlying machine, as such the tool can be bent to deliver semantics that are wholly surprising. I can think of no mainstream language that has that property. It's a shame.
Duff's Device works in D:
void send(short* to, short* from, size_t count)
{
auto n = (count + 7) / 8;
final switch (count % 8) {
case 0: do { *to = *from++; goto case;
case 7: *to = *from++; goto case;
case 6: *to = *from++; goto case;
case 5: *to = *from++; goto case;
case 4: *to = *from++; goto case;
case 3: *to = *from++; goto case;
case 2: *to = *from++; goto case;
case 1: *to = *from++;
} while (--n > 0);
}
}
The `goto case;` is D's way of saying "fall through to the next case".Nested functions are a little gem. I stole the idea from Pascal. I've since discovered that they can nicely clean up spaghetti code that C engenders, without losing efficiency.
Nested functions would fit into C seamlessly, I sometimes wonder why the C people don't adopt it. I can only assume they just don't realize how sweet they are, which is understandable because you just have to use them a bit to see it.
The original pascal based Mac toolbox APIs encouraged nested procedures in modal contexts - but Nested procedures couldn’t escape their outer frame.
D's nested functions (like Pascal's) have two frame pointers - a dynamic frame pointer (like C has), pointing to the caller's stack frame, and a static frame pointer pointing to the enclosing function's stack frame.
This allows the nested function to access the enclosing function's variables.
It's a bit tricky to set up, but works a treat.
1. you cannot "goto" out of the nested function
2. you can declare them "static", which means they don't have a static link and cannot access the enclosing function's variables
3. if garbage collection is enabled, you can pass a pointer to the function out of the enclosing function's scope and still safely access its variables
4. they work in `extern (C++)` functions, too
And so here we are.
2017 https://news.ycombinator.com/item?id=15679205 - particularly https://www.lysator.liu.se/c/duffs-device.html
2016 (a bit) https://news.ycombinator.com/item?id=12758686 and https://news.ycombinator.com/item?id=10830744
2015 https://news.ycombinator.com/item?id=10175901
2015 (in Go) https://news.ycombinator.com/item?id=8990742
2012 https://news.ycombinator.com/item?id=4360306 (note the personal anecdote)
2010 https://news.ycombinator.com/item?id=1896643
"C's default fall-through in case statements has long been one of its most controversial features; Duff himself said that 'This code forms some sort of argument in that debate, but I'm not sure whether it's for or against.'"
The main program is one big, big function with a big, big loop and switch in it. Each place where it does something that would (otherwise) block, it sets a state variable to match a case label right after, does a non-blocking version, and breaks. (This is done with a macro.)
After the break, it stashes the state, and if anything is ready, switches to that state and re-enters the switch. Otherwise, it sleeps waiting for work that can proceed.
As a colleague remarked, "I love it, and hate myself for loving it."
switch(override){
default:
printf("unexpected...\n");
// fallthrough
case 0:
if(z_bits&4 || N_flag){
case 100:
handle_100();
}else if(x_bits & 0x80){
case 200:
handle_200();
}else{
case 500:
handle_500();
}
}
It's cut down a bit for Hacker News. The original had about a dozen lines of code where each handle_XXX function now sits.It's unusual because in structured languages you expect the lexical structure to be entirely hierarchical (you wouldn't have an if block that somehow overlapped with a for-loop, for instance). Duff's device breaks that assumption because it uses one control structure (the switch) to jump into the middle of a second control structure (the loop), and lexically, the switch is both inside the loop and outside of it.
This is why lots of people when they first see it don't think it is even syntactically valid. A regular switch with fall-through doesn't violate the "hierarchical lexical structure" assumption in that way.
switch(n%4)
{
loop:
case 0: stuff();
case 3: stuff();
case 2: stuff();
case 1: stuff();
if(--n) goto loop;
}I needed this to run multiple stuff at the same time (for example printing while also doing network -- both serial devices on VX510).
Could have done this with setjmp/longjmp but it would not be as fun:)
It's mind-boggling to me that something like that can be "discovered". It's certainly "discovered" because it relies on a particular setup that relies on specific low-level features of C. Perhaps I would have denoted it as "invented" but the more I read about it, the more I realize that it probably doesn't qualify as something that was "invented" for the same reasons.
The article does say " At the time of the device's invention this was the first edition of The C Programming Language which requires only that the body of the switch be a syntactically valid (compound) statement..." which implies it isn't valid now.
Any correct parser based on any sane grammar should instantly choke on that.
> switch (expression) statement
>The usual arithmetic conversion is performed on the expression, but the result must be int. The statement is typically compound. Any statement within the statement may be labeled with one or more case prefixes as follows
> case constant_expression :
https://archive.org/details/TheCProgrammingLanguageFirstEdit...
So I thought the downside was you can declare “register” but it might not obey but then definitely can not use the & to get the memory address by using they keyword. Which on portable code register isn’t recommended.
So I suppose it’s not entirely fair to say all modern or all past support this. Right?
1. You're right that you can't use the & operator on a variable that was declared using the register keyword.
2. However, if you look very carefully at the example code, it actually follows this rule. In fact, it doesn't use the & operator at all. There's a lot of pointers all over the place, but no usage of the & operator.
3. You're also right that the register keyword is just a hint that the compiler is free to ignore.
4. Expanding on the previous point: modern compilers usually will ignore it, but in the olden days, compilers were not so good at register allocation, so this keyword was needed if you wanted speeds competitive with assembly. Since this is code from the 80s, no surprise that it uses this keyword.
5. Now that we've got all that out of the way: the register keyword is a complete distraction here, and has nothing at all to do Duff's device. Since modern compilers ignore it anyway, you can take the original example and just delete all uses of the word "register". The point of Duff's device is how to do manual loop unrolling without then having a special "pre-loop" or "post-loop" which takes care of "leftovers" because your array size was not divisible by 8, or 4, or whatever unroll factor you happened to use.
I also can't seem to get there with a plain goto. I can't jump out either. Probably the computed goto extension will do the job, but that is cheating.
To give just one or two examples of why this is doomed: imagine you want to jump "inward". If the inner function takes any arguments, you haven't passed them! And if you jump inward, and the inner function then hits a return statement, do you return to the outer function, or to its caller?
And after you resolve all such semantic issues, you need to actually implement all this, without slowing down code which doesn't use this feature.
And all this complexity doesn't buy you much because you have two other options:
1. just call the function to jump inwards, and simply return to jump out.
2. Don't bother with nested functions, and just inline the code.
There's also the C programmers that come from assembler for whom the construction expressed by Duff's device is very natural. They would have a hard time believing that somebody find it surprising. For those people, it is obvious that the code should compile and its meaning is clear, as can be mapped directly to the compiled code.
switch (count % 8) {
turns into jmp [QWORD PTR .L4[0+rdx*8]]
just like you'd do if you were doing the assembly by hand.