Libmill: Go-Style Concurrency in C
libmill.org
libmill.org
This "choose {" "end }" business is an awful way to design the macro. If you need nesting that involves a block, with prolog and epilog material on either or both ends, you just have to hide that into the macro:
selbegin;
out(ch, int, 42):
foo();
in(ch, int, i):
bar(i);
otherwise:
baz();
selend;
I don't understand the downvotes. I have 30 years of (continous!) C experience. This is the best practice for doing a macro like this.This "end }" business has improper nesting, visually. The choose is outside of the braces, the end is within; it is scatter-brained. There is no error checking for a forgotten end.
I'm looking at the implementation and it's looking quite weird! choose simply expands into mill_choose_init__, and end expands into mill_choose_end__. The definitions of these seem as if they should pair together:
#define mill_choose_init__ \
{\
mill_choose_init_(MILL_HERE_);\
int mill_idx = -2;\
while(1) {\
if(mill_idx != -2) {\
if(0)
#define mill_choose_end__ \
break;\
}\
}\
mill_idx = mill_choose_wait_();\
}
But, oops, look at the obvious lack of balance in the braces. mill_choose_init__ {
mill_choose_end__ }
we are putting a brace after the if (0) which balances the one after break, and the final missing brace that balances the one opened internally in mill_choose_init__.There are best practices to design this sort of macrology that don't require the user put braces in weird places to balance the inconsistencies in macro expansions.
Speaking as a professional, I would not pass this in a code review.
That we have to use a flaky preprocessor in order to get a half decent syntax is already a significant regret. We must not compound the regret by allowing the macrology to be less than as good as it can be.
Furthermore, my opinion here is, in fact a technical comment. Hidden open braces inside a macro expansion that are explicitly closed by ones written by the user, and vice versa, is actually a technical issue, not simply aesthetics. The two are not entirely separated.
The downvotes are probably because you’re expressing a naming bikeshed issue like your opinion is undeniably correct.
‘I would call the function ‘select’’ would be a reasonable comment.
It's fairly obvious to me that the author wanted that identifier; I'm just saying that the reasons against changing the name are weak.
libmill provides some wrapping for polling mechanisms; you'd probably be going through that in libmill-based sources.
The clash, if it happens, has very easy, localized workarounds.
Note that the library has a macro called end, which is a very common identifier in programs. Why didn't the author consider that a problem, but switched Go's select to choose?
sure, workarounds are easy, but that's not the point. your suggestion that it should be named the same as Go is a bit ridiculous. why is that even important? what is important is potentially confusing developers, and adding a new "select" into C would do just that.
the only mistake is Go using the word select to begin with.
I worked in Windows development shops where nobody knew select; but they could write WaitForMultipleObjects code in their sleep.
A strictly conforming ISO C program can use the select identifier however it wants, including as an external name for an object or function.
> your suggestion that it should be named the same as Go is a bit ridiculous
It's not any more ridiculous than wanting to have a library which imitates Go concurrency down to macros that imitate some of the syntax.
That's hard to believe considering the fact that POSIX-compatible select() is a part of the winsock API.
Are you aware that you're making that complaint in a discussion on Libmill: Go-Style Concurrency in C?
It's one thing to skip the submission to jump to the comment section. It's an entirely different thing to start mindlessly criticizing constructive criticism presented by fellow users while being entirely oblivious to both the subject and the context. That's just noise, and contributes zero to the discussion. In fact, it takes an awful lot from it.
Just because it's Go-style, doesn't mean "Exactly like Go, down to the keywords used".
That point was already discussed previously in the discussion, and it's pointless to continue repeating what has already been said, particularly when those arguments boil down to nothing more than a personal opinion.
It's a very common source of hidden bugs thanks to FD_SETSIZE.
If you want to select on file descriptor 1000, the bitmask has to include zeros for descriptors 0 through 999. Those get copied to the kernel and iterated over dutifully.
One is a minor esthetic problem, the other would be a nightmare to support and work around.
There are lots of API's in the world; you can't ponder about all of them when naming a macro. How do you know the next system vendor doesn't have a choose function? Or if not the next system vendor, then some third party library or module you'd like to use or whatever.
POSIX doesn't reserve the select identifier for use as a macro, unless the header file is included which defines it. It is not "complete breakage", and there are workarounds in that situation, one possibility being:
#undef select
#define whatever mill_choose_init___
The breakage only happens for code that uses either the select macro or the select function, if the headers are included in the proper order: the system headers first, then libmill.h. I.e. this by itself doesn't cause a problem (except in the improble case that <sys/select.h> defines a select macro): #include <sys/select.h>
// #undef select at worst!
#include "libmill.h" // with #define select macro
This does cause a problem, 100%: #include "libmill.h" // with #define select macro
#include <sys/select.h>
But you should never do that: do not include local headers before system or third-party headers, unless you're sure they don't define any macros.There is also no problem if the rest of the file uses the select macro. The only issue arises if the rest of the file wants to use the select function, and then we put in a workaround. You're not going to call the select function in hundreds of places, where you also need the libmill header.
TL;DR: it's not anywhere near a "nightmare".
Moreover, libmill defines a few macro with short names. Conceivably #define end could be a problem, as well as #define in(..) and #define out(...).
For instance if we have a "range.h" which has this:
struct range {
int beg;
int end;
};
and we do this: #include "libmill.h"
#include "range.h"
then libmill's end macro will screw up the struct range declaration, not to mention any place where we use end as an identifier. It's not an uncommon local variable name, like for pointers pointing to the end of a memory range.Note that the code already has some multiplexing gadgetry there, switchable between Linux's epoll, BSD's kqueue and the portable poll (see epoll.inc, kqueue.inc and poll.inc).
Holy shit.
Standard practice in C is to prefix your functions with their 'namespace' to avoid collisions. In this case, I don't even like that they used "choose" instead of "mill_choose" or "mill_select".
But redefining select() is a whole other level of inconsiderate.
Did you notice the #define end, which is a common name for struct members and local variables. Redefining select is certainly not a whole other level of inconsiderate in that light.
The caveat is that whoever uses this should include it last, after any other headers, and then deal with the macro clashes: don't use "choose" or "end" in their code and so on.
One way to mitigate this is to have these macros in a separate header, say "libmill-macros.h", so that the rest of the API is available without them.
Don’t do this, ever. Just because it’s technically possible doesn’t mean it’s a good idea.
You're making ill-informed accusations.
Please check libmill and pay attention to how it has been implemented, particularly the design choices covered in this thread, and afterwards re-read OPs comments.
Local shadowing is completely different to global shadowing.
I don't know what's happened to HN these days. The sheer number of kids who have come out of the woodwork to flame this guy in different ways on different threads -- all with the same level of ignorance on a really simple topic -- is nothing short of deflating.
To claim everyone who doesn't share your opinion are kids that just came out of the woodwork and are ignorant is not helping the discussion either.
Grandparent made a controversial suggestion, and the discussion shows it.
I have the 1999 version of the standard in PDF form handy. Let's see,
7.1.4 Use of library functions
1 [... snip ...] Any macro definition of a function can be suppressed locally by enclosing the name of the function in parentheses, because the name is then not followed by the left parenthesis that indicates expansion of a macro function name. For the same syntactic reason, it is permitted to take the address of a library function even if it is also defined as a macro. The use of #undef to remove any macro definition will also ensure that an actual function is referred to.
Nothing dubious about using #undef to remove a macro definition of a library function.
There probably isn't a select macro; the #undef is just in case there is one, so that when we #define our own, we don't get a diagnostic about a redefinition with a different token replacement sequence.
No, you're not. Frankly I've never seen select(2) properly invoked in more than one place in an entire project, and neither have you.
That's why a C professional isn't really phased. Sure, there are about a million english words in the OED and some more creativity would be fantastic. This still isn't a problem, on the facts. It's not an opinion.
The point about the danger of mixing explicit brace opening with implicit macro brace closing is prudent, though.
This discussion is on a library that advertises itself as offering "Go-style concurrency in C", and the OP presented a valid case about an aspect of the library that misses its whole design basis.
Quite obviously that's not bikeshedding, but valid criticism regarding the very basis of the whole project.
If someone someday makes a name that collides with yours, that's a problem for that "someday". Avoiding today's problem today seems pretty valuable.
POSIX systems in fact go out of their way to support conforming ISO C programs, which are permitted to contain things like:
void select(int x) // not in any ISO C reserved namespace!
{
selection = x;
}
In the case of the GNU C library how it works is that select is a weak symbol aliasing for __select. If anything in the library needs to use select, it calls __select, so as not to be derailed by a user program that redefines the weak symbol.Issues with macros are less severe compared to clashes in actual external linkage. No weak symbol support needed in shared libs; just something has to be #undef-ed here, or an order of headers rearranged there.
> To software written in C, the syntax or naming choices of other languages are really quite irrelevant.
However, at least one person out there thinks that the syntax and naming choices of Go are relevant to software written in C, so they wrote this thing called "libmill".
Oh wait ... what were we talking about?
He merely questioned the ironclad value of your opinion as gospel “just because you have a lot of experience”. He didn’t even say it’s definitely wrong , but that it’s not necessarily always a good argument. That is completely fair. You can’t go and ask OP now to prove that you somehow hold a mistaken belief.
You want to make controversial claims? Great, please do. Love it. But you’ll have to back them up with more than just your say-so.
Experience accounts for nothing in iconoclasm. Thankfully.
(And I actually think the points you raise, themselves, are interesting, and great food for thought. For the record.)
I've exhaustively explored pretty much the entire space of this. You can whip out and environment with a compiler and try those things I wrote about.
There is literally nothing more to add.
chan_select(2) {
chan_case_send(c, &foo, 0):
do_stuff();
chan_case_recv(c, &bar, 1):
do_other_stuff(bar);
} chan_select_end;
But the implementation (https://github.com/iriri/libcee/blob/master/include/cee/chan...) is both horrifying and not as efficient as just doing the sane thing: chan_case cases[] = {
chan_case(c, CHAN_SEND, &foo), /* unfortunately still a macro but it's basically just a struct literal */
chan_case(c, CHAN_RECV, &bar),
};
switch (chan_alt(cases, 2)) {
case 0:
do_stuff();
break;
case 1:
do_other_stuff(bar);
} choose { cases... }
using https://www.chiark.greenend.org.uk/~sgtatham/mp/ techniques.In this case, I think you could make it work without any macro at the end, just a closing brace, without changing the rest of the syntax (including how the label-like constructs implicitly open a scope, which I think is a bad design but whatever). Something like this (for simplicity I've omitted backslashes and the concat calls needed to make label names unique):
#define choose
if (1)
goto register_options;
else
while (1)
if (1) {
/* We get here once the registrations are done */
void *target = mill_choose_wait_();
goto *target;
} else
register_options:
if (0) {
#define in(ch, type, var)
} else if (({goto register_N; did_register_N: 0;})) {
type var;
register_N:
mill_choose_in_(ch, &var, &&picked_N); /* computed goto because why not */
goto did_register_N;
picked_NIn here, nobody knows (and more importantly nobody cares) you are a dog.
Coming from Clojure, I wished for something like core.async on microcontrollers for a long time — a way to convert most of my state machine into sequential code, with the complexity hidden, all while keeping everything in C (converted/generated during compile).
Isn't that the norm for state machines in C?
while (cont) {
switch (v) {
case S_STOP:
cont = 0;
break;
...
}
}
Or are you talking about inlining sequences of transitions into eachother? I've been avoiding core.async so I don't really know what it does that has to do with this.* do one thing
* wait for the result of one thing (for example, an event from an interrupt)
* take the result and feed it into another thing
* wait for the result of another thing
So the code looks sequential. It's easy to write and easy to understand. But the entire 'go' block returns immediately: the whole thing gets converted into a state machine (which you don't normally see). That way you don't have to manage the complexity of the state machine yourself.
Note that this is not the same thing as threading. Also, it does not require threading at all, for example core.async works just as well in ClojureScript, even though Javascript has no threads.
They provide a form of stack-less, co-operative multi-threading and are aimed at microcontrollers.
I came across them by chance, and have used them on an ESP8266 – it's really made everything feel much more clear and far less confusing.
You do need to wrap your head around the trick, but that really shouldn't take long. You also need to put any state that you expect to be preserved between calls of the function into something more persistent – just like with state machines. The examples all use static variables, but I am happier using a struct pointer to maintain state.
If you can’t afford the memory for a dedicated callstack per thread then there are the other event driven models that had been mentioned here. However one loses out one real-time capabilities and priority handling by using those compared to a preemptive RTOS.
Actually, there were no release since 2006, when the last bug was fixed I guess :+)
https://twit.tv/shows/floss-weekly/episodes/358
And HN discussion from 4 years ago: https://news.ycombinator.com/item?id=10585505
Are there any plans to support multicore? I noticed one of the examples addresses this by using fork(). Multicore is something goroutines > 1.5 support.
libdill (also by the creator of libmill) has some features that libtask does not.
[edit]
Also one should note that libtask predates Go, and so the API definitely feels very different. Libmill seems to specifically have a goal of making the usage feel "go-like"
Specifically, the same thing we're talking about here already happened with Alef[0], a CSP-based language, which was scrapped in favor of a C library[1] (man 2 thread) in the third edition of the Plan 9 OS[2].
[0] http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/
What would be nice would be a defer function for closing channels and things.