Show HN: A pure C89 implementation of Go channels, with blocking selects
github.com
github.com
2. This is not C89; at best GNU C 89, because a variable is declared after a statement:
{
if( msgLen == 0 )
msgLen = 1;
/* queueLen == 0 is an unbuffered channel, but we still need one slot to transport the message */
CspChan_t* c = (CspChan_t*)malloc(sizeof(CspChan_t) + queueLen*msgLen);
To help enforce that you're actually writing C89, you should set your compiler to C89 and turn on whatever additional diagnostics may be required like -pedantic.Fair enough. A "pure C89" program should be expected to work in Turbo C on MS-DOS.
EDIT: I now have changed the repository description to "A pure C (-std=c89) implementation of Go channels, including blocking and non-blocking selects.", so hopefully this idiotic discussion will come to an end.
> nothing but an academic quibble
Oh man, you had to give away the secret! Now everyone knows that this is just about how some choice of words in a title doesn't correctly relate to the content.
It's always nice and very motivating when you spend your free time on a project for the benefit of the public and are then publicly defamed by some busybodies with some irrelevant side issues. Thank you all very much for that.
Concerning your attitude please consult the guidelines of this forum which state "Plese respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize. Assume good faith. Please don't pick the most provocative thing in an article or post to complain about in the thread. Find something interesting to respond to instead." Maybe next time.
A common reason for coding in C89 is avoiding mixed declarations and statements. That's one of the most prominent differences between C89 and C99. The GNU compiler won't catch that unless you use -pedantic or else the more specific diagnostic flag -Wdeclaration-after-statement.
This one actually bit me, since in some projects where I was actually trying to avoid declarations after statements, some uses accidentally snuck into the code due to being undiagnosed! I thought that may be the case here: the author wants to work in C89, but is blind to accidental deviations from the dialect.
So that is to say, I didn't think that the title of the submission needed fixing so much as rather the code: make the code be C89, if you believe you are working in C89 and want to be doing that for some real reason. (Turns out, to my surprise, you don't know what C89 means and don't care, so, okay, never mind.)
I also pointed out that <memory.h> is a nonstandard header. <memory.h> is where AT&T Unix systems in the 1980's put memcpy and friends. ANSI C89 standardized those and put them in <string.h>. Glibc happens to have a <memory.h> header which includes <string.h> for you.
Code which has to work on such old systems that it is necessary to use <memory.h> should probably also be written in classic K&R C without prototypes. (Or with function definitions written without prototypes, with the headers optionally turning on prototypes using a macro, like int func PROTO((char *)); where PROTO(X) just expands to () for a non-ANSI compiler.)
Words mean whatever the people using them use them to mean; they can evolve as their usage changes, etc.
Jargon terms are defined, once, in some journal paper or textbook or standards document; and then anyone who uses the jargon term thinking it means anything other than that original definition, is wrong. Jargon terms are prescriptivist by definition — that's the point of them, the thing that make them useful, the reason people invent them.
C89 is a jargon term. It it is an abbreviation of "ANSI X3.159-1989", the name of an immutable, public-accessible document written back in 1989.
---
That being said: although "C89" itself has an exact jargon meaning, these aren't well-defined jargon terms:
1. what it means for something to be a "C89-supporting compiler"
2. what it means when a piece of code says that it requires only a "C89-supporting compiler"
However, given a common-sense understanding of these concepts as they would be defined to be useful to a working programmer or OS distro library packager, I would expect the following interpretations of these concepts:
1. a "C89-supporting compiler" is a compiler that supports a superset of the C89 standard, such that it can at least compile code that conforms to the C89 standard, to object code that has semantics that conform to the C89 standard. Such a compiler MAY (in the RFC sense of "MAY") be able to compile other code as well — perhaps with effect defined in some other standard; or perhaps with effect defined by no standard at all.) This is an "at least" guarantee — a "C89-supporting compiler" supports at least C89.
2. code that states that it requires a "C89-supporting compiler" (with no other specified requirements), should be able to be compiled by a hypothetical compiler that supports exactly and only the features and translational semantics defined by C89 standard. You should be able to implement a new compiler from scratch, to the C89 spec, and then compile this code using it. The code should be portable to any arbitrary compiler that claims to implement C89. This is an "at most" guarantee — such code will invoke features such that it requires the compiler to at most have 100% adherence to the C89 standard.
imagine this title
"A pure C implementation of Go channels, with blocking selects";
it means exactly the same; and now add the C version in use to be more specific; hope this helps.
Now some of the first comment chains people will see are hostile interactions between the author and commenters.
For someone considering adoption of the tool, feature requests, or pull requests, they are likely to consider how communicating with the author might be similarly hostile should disagreements arise.
It is unfortunate people often miss the forest for the trees in PRs and on hacker news with comments like the one to which you replied. It is easy to critique and difficult to create, so having people critique very small aspects of a project you created without acknowledging the actual project itself is irritating, but I still think either ignoring them or providing your rebuttal without malice will always be more effective for evangelizing your work.
In the comment you linked, that post received some similar feedback as well. I hope this did not come across as judgmental. It’s an area I also have worked on in my personal life and this was more so an attempt to share my thoughts about the negative ways this communication style has impacted me before.
Anyways, this project is a cool idea and I have respect for the time and effort you put into it. Wishing you well going forward.
I come from a culture where integrity and values play a role and where you talk things out and don't have to hide your views behind diplomatic formulas or pseudonyms. I also find it interesting how you assessed my vote. English is not my first language. I have tried to make my point clear in every respect to avoid the expectations going in a wrong direction due to misunderstandings. I kept my tone decent and did not personally attack or insult anyone.
But the reactions of my interlocutor made me realize that my efforts were likely in vain. In this respect, I agree with you that in some cases it is best not to respond. But this is HN and here is actually a good culture of discussion, supported by decent moderation, where arguments should not simply be ignored up front.
I agree that a dispute can deter potential collaborators; but on the other hand, if I just leave defamations about me or my work unanswered, this has the same negative effect. In the present case, I continued the argument to such an extent that my position was sufficiently elaborated and documented, and from then on I no longer responded to the further allegations.
Volunteering open source only works in a culture of mutual appreciation. It could be observed that in recent years appreciation has increasingly given way to an attitude of entitlement (which is a possible cause for nitpicking), especially among younger people. I think it is quite appropriate to make people aware of this fact and its consequences - and it may well take place at the top of an HN discussion.
Some thoughts:
I've often thought that unbuffered channels would cause scheduling thrashing - higher latency and lower throughput because you're swapping between stopping and starting processes blocked on a channel frequently. If you're sending just a small piece of data like 64 bit integer at a time, or any kind of pattern where you're using threads to break up work into tasks, this is too small breakdown of task to really scale multithreading. Want to communicate something that causes a LARGE AMOUNT of work on the other thread, to keep the processor busy. But there's balance between latency and throughput, if you send a big task you get higher latency to react to the next task but better throughput.
Walking through my thinking and help me understand: If you have multiple channels that you could read from in a select call but none are ready, could you block that select instance and process a different process where other selects are potentially waiting? This is similar to blocking a goroutine or a Rust async Future task in Tokio that needs to be waked. I think this would need a scheduler. EDIT: Your scheduler to switch between "select instances" or what is running in a thread is the OS.
From my experience, the most versatile approach for the wait on channels is best implemented as a spin lock with exponential backoff, up to a threshold that yields - effectively letting other threads perform their own select. Note that most of the time, if you are targeting latency, you would try not to get more workers than cores, meaning you would pin each worker to a core, thus not really benefiting from yielding on select.
> But there's balance between latency and throughput, if you send a big task you get higher latency to react to the next task but better throughput.
Agree, but that's mainly a client concern, the queue can propose multiple APIs. Most queues handle that with a "burst" mode, where you pop or push multiple messages at once instead of 1 by 1, to balance throughput vs latency.
Also measure before you implement this yourself, to my knowledge this is infact what the linux mutex does anyway.
I'd recommend adding some additional documentation about the API. You mention trying to keep dynamic allocations to a minimum (which I like to hear) but it would be handy to have documentation stating which functions allocate and how much they allocate. Actually, more documentation in general would be nice, particular relating to edge cases. In Go I know that sending to a closed channel will panic, for example. But what will your library do? Return some sort of error message? Silently drop the message? Segfault? Definitely something you want prominently documented. Especially anything that could potentially segfault should be highlighted.
Oh, and some benchmarks would be very interesting!
As far as I understand the differences between CspChan and libmill might be that libmill also implements lightweight tasks (coroutines) and everything that goes with it (IO multiplexing, async timers, etc), while CspChan uses OS threads?
I do want to say though, if you're considering using this for a production system: it's worth also considering ZeroMQ inproc sockets[0]. They allow for very similar semantics to this, with the added benefit of trivially being able to migrate to an inter-process / network channel, by just changing the URL to bind / connect to.
Kind of unfortunate as other more handrolled solutions (depending on use case) can easily be <10us.
Feels like you really need to be using a lock free ring buffer that only supports 1<>1 communication if you really want to go fast. (p50 of <1us with very compressed tail latencies)
Go, for its part, has its receive operation, `<-`, return a status indicating if the channel is still open, as in `v, ok := <-ch`
There's nonstandard nomenclature in "_dispose"; if the constructing function is called _create, the common pattern is to pair it with _destroy.
typedefs ending with _t are reserved for standards extensions, though these days people ignore this a lot because it feels like general practice to add _t.
There's no way to pass in a custom allocator, or logging callback, or set some debug logging flags. Maybe something to tackle later.
Both _select functions fall short in that they do not allow select'ing on channels + other file descriptors simultaneously. General library design practice is to have the library return a file descriptor that can be used in whatever event loop the application already uses. (The fd can be a dummy pipe or eventfd.)
All of these things are in the header, so they matter the most as they set ABI and API for any user. Unfortunately getting them right as early as possible is important to avoid breaks; yet very hard to do since often API aspects only become clear after a library has non-trivial users.
[edited to tone down a bit]
It matters because the return value sense is different; an "int" return for a status-ish thing in a modern library generally means 0 for success, nonzero for error codes. "bool" is the other way around, 0 for failure-y. In this case it's a status retrieval function so it's pretty clear that it's intended as a boolean but it'd still be better to actually make it bool.
I didn't see the original comment and I may take this better since it isn't my code but I don't ser this comment as negative.
If gives clear criticism which I can easily brush off as the way I want my library to work, but that when I read it I can agree with.
One if my major gripes with many libraries doing asynchronous work is that they just blindly assume I will be using their blessed event loop when in reality I already have an event loop. Which means I now need to put their code in a new thread, regenerate events and pass that to my event loop, that tends to add complexity.
There are no "mods" on HN. There is one singular admin, dang, who pretty incredibly manages both the tech and the community.
In terms of people who have the power to downvote comments or flag stories, these are just regular HN users. There is a karma threshold at which downvote and flag links appear, but there is otherwise nothing special about this ability.
Some folks may think I'm splitting hairs, but I think it's important to clarify, as I often see comments blaming the "mods" for e.g. a post that was downvoted or flagged, as if the mods are some shadowy cabal (which can be the case when referring to Reddit mods). If posts are downvoted are flagged, it's because the community at large didn't like what you had to say.
.. but likely more than either of us get paid to comment.
You're providing a ton of code style feedback. That's fair, code style is important, I'm not saying you shouldn't. But surely when providing style feedback in public you could take a friendlier tone :(
Thanks!
bool is actually #define bool _Bool. You have to include <stdbool.h> to get it. That's ugly enough not to want to use it.
I'm looking at the April 2023 draft of ISO C. Under "Relational Expressions" I see that the result of an expression like x < y isn't bool, but ... int.
Here is the exact wording, minus a footnote reference:
Each of the operators < (less than), > (greater than), <= (less than or equal to), and >= (greater than or equal to) shall yield 1 if the specified relation is true and 0 if it is false. The result has type int.
int is still the Boolean type in C, and null pointers and zeros are still falsy, relational expressions yield 0 or 1 of type int, and #define bool is just a sham for anal retentives.
> typedefs ending with _t are reserved for standards extensions
That is inaccurate. It's POSIX that reserves this namespace. Even if you're targeting POSIX, the reservation has next to no practical meaning, and can safely be ignored.
Here is why. What POSIX says is that when new type names will be introduced in the future in POSIX, they will have _t as a suffix. Well, so what? New type names in ISO C will also have _t suffixes, yet ISO C doesn't say anything about that being "reserved".
When a new identifier is introduced, it has to start and end in something.
Whenever POSIX introduces a new public identifier in the future, that identifier will either start with "a", or else with "b", or with "c" ... does that mean that we should stop using all identifiers in order not to tread on a reserved space?
When you have a language without namespaces/packages, you just live with the threat of a clash and deal with it when it happens.
Name clashes are not just with standards like ISO C and POSIX but vendor extensions and third-party code.
The mole is only a problem when it rears its head, and that's when you whack it.
Sure, which is why it got changed to a keyword in C23. I'll agree this should have happened earlier.
The return type of comparison operators is entirely irrelevant; you don't build APIs by reference to the return type of a comparison operator. It makes a difference to the reader whether your source code says "int" or "bool", that alone is all the reason anyone should need.
> That is inaccurate. It's POSIX that reserves this namespace.
You got me there. However, the question is, why would you add the _t? It serves no purpose. Typedefs have their own namespace anyway, you're not working around some possible collision by adding the _t.
That said I've already noted this is a commonly ignored aspect. I suppose it "feels" better/correct to some readers. I will happily agree this is the weakest point of my feedback either way, yet I still rather point it out and have people learn more details about this.
It is entirely relevant. The boolean type of the language is whatever is the type of (0 < 1).
> Typedefs have their own namespace anyway
Typedefs positively do not have their own namespace.
If you write
#undef getc
{
typedef char getc;
}
you cannot call the getc function in that scope; it is now the typedef.It is true that it's more correct to say "struct names have their own namespace", though I think we can infer what's actually meant here fairly easily.
That is false. If we declare an "enum foo", which is not a struct, then we cannot have a "struct foo" in that scope.
Alternatively, and especially in a library API, using bool is a clear specification of semantics.
An int parameter could have many meanings. An int return value similarly. In both cases, using "bool" makes the usage instantly more clear, without the need for comments, or man pages, or Doxygen, or super_long_parameter_names, or whatever.
I think it's useful.
Integer promotion should go, like implicit cast for anything except void* and literals, but we are missing a dynamic/static casts syntax explicit split. Only one loop keyword, loop {}, no switch, no anonymous block, no "a?b:c" operator. typedef has to go, (and typeof,generic,etc), the variable arguments of preprocessor function should be defined once and for all. __thread must go as tls should be managed dynamically and explicitely with the system interface, never statically and hidden by the runtime. Only sized primitive types (u8/s8...). That said, anonymous union/struct are very nice for complex memory layout.
And all the things I am forgetting right now.
With the pre-processor and coding discipline you can get close to that already, but I was told that what I am describing is basically the simplicity of rust syntax, true? Namely it is easier to write a naive rust compiler than a C compiler?
This operator is very useful and uncontroversial in Perl, it's normal (and quite readable) in selecting a value for assignment. One is expected to use good practices with it, but it is nice.
Is the problem the fact that in C you can use it instead of if, selecting not values but actions, like (a > b) ? printf("A") : printf("B");
const value = cond ? a : b;
Using an if instead would mean you can't make it const, and is usually more convoluted to read.For pretty much all other use cases, I agree that ifs are better.
For example, the following F# code (you can run `dotnet fsi` in a terminal to try it out, if you have dotnet installed) is equivalent to a ternary expression:
// the value of x is "is_not_lower" after evaluation (x is a constant/immutable value, just like a 'const' value in JS) let x = if 0 > 1 then "is_lower" else "is_not_lower"
Which is equivalent to the following (in JS): const x = 0 > 1 ? "is_lower" : "is_not_lower"
In F# (and also Kotlin which I linked before), you can have clean code with if-elif-else ladders while the equivalent (using nested ternaries) would be hard to read. For example:
// the constant/immutable variable "comparison" is either "HIGHER", "LOWER" or "EQUAL" depending on the branch taken let comparison = if a > b then "HIGHER" elif a < b then "LOWER" else "EQUAL"
In JS land (and also most common programming languages which have plain if-statements), I agree a ternary would often be preferrable as long as it's not nested.
a = if x then y else z
is just a more verbose version of a = x ? y : z;
Raku uses yet another form of the same operator: a = x ?? y :: z;
Syntactic sugar :-) a = x ?? y !! z
And that's because a single : is used for many other semantics in the Raku Programming Language. And ?? and !! standout better in code.I think I made this mistake because :: was used in one of the drafts for Perl 6, but the final specification uses !!
a = (x < 5 ) ? 'LESS'
: (x > 5 ) ? 'MORE'
: 'SAME';I have also seen them used cleanly and beautifully, but that is a rare occurrence.
For instance, a "feature" of significant and pertinent impact missing, in the list I provided, load and store from unaligned memory address should have their own keywords as this is really important, even though modern hardware micro-archs tolerate that at a cost. Anybody using that abomination of "struct packing" C extensions should think again: C has been meant for aligned access by default, namely unaligned access should be at a syntax cost, and should pop up from the syntax and be HARDCORE explicit. I don't even think rust syntax is that clean, am I wrong? (hopefully)
For instance in linux printf, I did run in a beautiful (irony) "aligned" attribute on a stack variable. I was reading the code and I though there was a bug since I did not read the variable declaration as I was very mistaken that the variable name was enough... Yeah, should be banned from the kernel (hopefully, rust is careful to avoid that abomination in its syntax).
When I have "packed" data structures, I am, usually, careful at having everything "byte" defined and use explicit macros for Xword loads and stores.
"Letting the compiler do that for you" is dual edge sword. And I don't even start on the _Generic abomination, which creates so much type indirection you don't know what you are dealing with unless you read a ton more code.
You must be joking.
You know the answer?
There are plenty of constrained environments where this is indeed a feature to advertise, namely older machines and embedded systems.
Anything from the 80s/90s isn't really worth supporting. We gotta draw the line somewhere.
It's become such a common practice to suffix your typedefs with _t it's practically a de facto standard at this point.
Since everyone's already namespacing types with some kind of prefix, I don't see the problem nor have I ever experienced a negative consequence after decades of writing C this way.
Oh come on, let's not play the language police... You are entitled to your language likings, others are not. C89 is simple, straightforward, no bells and whistles, and some people like that (I do).
> There's nonstandard nomenclature in "_dispose"; if the constructing function is called _create, the common pattern is to pair it with _destroy.
"Nonstandard" says who?
In my 20 years of C I've seen all possible combinations of _init/_create/_new/_alloc _free/_deinit/_release/_delete/_destroy/_dispose. As long as it's consistent across the codebase, it's fine.
> typedefs ending with _t are reserved for standards extensions, though these days people ignore this a lot because it feels like general practice to add _t.
Not really. The usual convention for _t is to differentiate between raw struct names and typedef structs, such that you know whether you have to prefix "struct" during declaration. i.e. `struct foo {}`/`typedef struct {} foo_t`
> Both _select functions fall short in that they do not allow select'ing on channels + other file descriptors simultaneously.
Good point
First, a commit to change the docs to say it's a pure C99 (or C11 or C17) implementation.
Second, for each of your points, a single commit fixing each style issue (_dispose -> _destroy, typedefs).
Seperately, another MR with the commit for _select improvements.
Or maybe just send the C99 one first, and if that gets rejected, don't bother with the rest.
Going for C89 for a truly portable project is probably fine.
and by a dedicated few to this day: https://lists.gnu.org/archive/html/tinycc-devel/
It's not much but it's not dead.
How generous. The comment reminds me of this passage in C. Northcote Parkinson's famous 1957 book:
"The man who is denied the opportunity of taking decisions of importance begins to regard as important the decisions he is allowed to take. He becomes fussy about filing, keen on seeing that pencils are sharpened, eager to ensure that the windows are open (or shut), and apt to use two or three different-colored inks."
How can it be that in HN a comment like this, which is all about trivialities and the personal sensitivities of the commentator, and which contributes nothing relevant to the matter at hand, receives the most upvotes? Is this comment somehow pinned to the top instead?
Many here seem just disparaging about Reddit, but if you want to see an actually useful and intelligent comment from someone who actually knows something about the subject, take a look here: https://old.reddit.com/r/C_Programming/comments/18fzr1a/a_pu.... I'd expect at least that level from comments on HN.
I feel like purposely emphasizing C89 in your project name is meant to highlight some sort of greybeard or serious hacker aspect to it. Presumably the poster was thinking along the lines of "if you're trying to show how proficient you are with a sharp knife, here's a list of all the ways you cut yourself with it."
It's unfortunate that you pose this as a rhetorical question, it has an actual answer: replace "pure C89" with "C89 compatible", wrap it in braces, and move it to the end of the title.
The message you're communicating by putting "pure C89" right front is very different from having a "(C89 compatible)" at the end: the former sounds like C89 is somehow an unequivocally good thing ("pure") while the latter makes clear that it is to address an unfortunate reality of engineering life. It would also distance you from C style and quality focused comments.
> which is all about trivialities and the personal sensitivities of the commentator, and which contributes nothing relevant to the matter at hand
The select API concern seems relevant and actionable.
Not even English has the "purity" that you subliminally presuppose in your (entirely unnecessary) assertions. See e.g. https://www.merriam-webster.com/dictionary/pure?utm_campaign.... And not a single one of your arguments had to do with "language purity"; instead you nit-picked about naming and features you miss; so what? But hey, it's open source: impress the world with your own version of the library.
I didn't consider the aspect of which modern toolchains are still attached to C89, but using a language spec that's 35 years old has some relationship to the grayness of one's beard. But in any case I used the term greybeard to evoke the aesthetic of "fuck modernity, embrace terse C, suckless software" etc.
Or indicating that it will work if you want to fold the code into a C89 project, which is not exactly a rare concern.
Even if you think they wasted their time, they didn't waste your time, and I value the existence of things like this, which means even if you don't, at least some others do.
The more reasonable discussion then becomes: fix the non C89 issues so the statement becomes accurate, disclose that it uses extensions, or as some have constructively suggested consider just targeting C99 rather than GNU89. Not sure why you think criticism of something not being what it claims is inappropriate.
They're mostly legitimate, good-quality feedback for a new C library.
_create/_destroy -- yes; custom allocator, logger, debug level setting -- yes; returning a file descriptor -- yes; the implied suggestion to move stuff out of the header -- yes.
This is important feedback. These things will require a breaking change down the line unless you tackle them before 1.0.
I encourage the author to take this onboard: it'll make your own life easier and your library a better C citizen.
My feeling is that they're probably too low-level and error-prone, and you really want higher-level structures like worker pools or actors. So as a concurrency building block they'd be on the same abstraction level as pthreads or Java monitors -- nice and flexible for building on top of, but too finicky to be used directly in application code.
But I'm not a Go expert, and maybe Go programmers do successfully use channels directly for application code?
One thing that struck me about your comment is referring to actors as a “higher level structure” compared to channels. Comparing Go channels with the actor model is more a matter of apples and oranges than one being more high level than the other. Go is built on the Communicating Sequential Processes (CSP) model: https://en.m.wikipedia.org/wiki/Communicating_sequential_pro... . See the comparison to the actor model in that article if you are curious. The essential idea to CSP is that you have independent processes that only communicate with each other via these channels. This is in comparison to the actor mode where each process has its own dedicated “mailbox” that it can receive from (but can send messages to any other actor it wants). I think that wiki does a better job of outlining the differences than I can in a comment, but essentially these are two similar but subtly different ways to model parallelism.
Personally, I find the CSP model a bit more intuitive than the actor model (though admittedly I have not worked with the actor model as extensively). For the basic idea of “make a process that receives input data, processes it, and submits output data somewhere else” CSP and the actor model are more or less identical. But for more complex use cases, like a process that can receive multiple kinds of messages from different places, or having multiple processes all handling the same stream of incoming data (aka load balancing) I find the CSP model more intuitive. This might be personal bias though. In particular I’ve never seen an actor model with static types and how that would handle different types of messages coming into the mailbox.
Short answer: channels for concurrent programming is definitely a good tool.
That’s really the question I was trying to ask -- is the CSP style suitably intuitive for general-purpose use? I personally find other primitives easier to use, but that could well just be my bias.
(I’ve done some academic work around process calculi, a long time ago now, and I always found the CSP style much more awkward than CCS / π-calculus.)
I’ve never seen an actor model with static types and how that would handle different types of messages coming into the mailbox.
I think it fits really well in an OO approach, where your messages just become method calls on the actor object -- I believe Swift does that.
That transformation of a method call into a message struct is a kind of defunctionalization, which is a really useful technique (in e.g. Redux) but usually involves a ton of boilerplate. Having a core language primitive that gives you defunctionalization and concurrency, now that’s interesting. I feel like that’s something I would end up building by hand on top of channels (I haven’t done that in Go, but I have for web workers).
That’s definitely true. Here’s what I think of as a motivating example: consider a process that takes input data in an input channel, provides some sort of enrichment, and writes the result to an output channel. Simple, right? Now imagine that enrichment requires some sort of configuration data. And that’s mostly static, but there are regular updates to that configuration that are provided by some sort of side channel. In the CSP model, that’s fine: have the process read from two channels, one with input data and the other with updated configuration data. In the Actor model, you have to read from your mailbox, check if this is an input message or a configuration update, and act accordingly. There’s no sort of OO approach that makes that switch statement feel “natural.” I feel that there’s no point into trying to shoehorn both that input data and configuration updates into the same message queue. Any attempt to do so in a clean way is basically just putting arbitrary functions onto a queue.
That said, I recognize that, beyond certain edge cases, the two models are logically equivalent.
This is what sum types give you, or dynamic handler registration if you need to support runtime-configurable message types (perhaps plugins or something).
There’s a good article about Tokio tasks as actors: https://ryhl.io/blog/actors-with-tokio/
Of course, these still use channels!
I think it's also students; I think some people are still taught C as their first programming language. If you go on r/C_programming on Reddit, a lot of it is filled by very basic questions from what seem like beginners; I suspect they're students trying to do their homework.
- People who work on projects which require low-level access, potentially mixing in assembly code (drivers, OS, etc), porting to different architectures, real-time, high-precision, control of timing, lack of MMU, etc
- People who want to perform specific actions, like a specific OS syscall, that may not be exposed [easily] by a higher level language in the way the programmer wants
- People who want to write very fast/efficient code
- Legacy code maintainers
Not too long ago I busted out some C to create a stripped binary for a job interview round. The interviewee needed to diagnose what the binary was doing and why and report back. Not all that common for most programmers today, but I expect a good amount of detail from a senior-level systems candidate.
I call that productive.
Further, you have the smallest API/ABI stdlib to work through the quirks of of any language. Again. Productive.
I, of course, take the precaution of allocating time to blow up 5/8 of a solar system/a star into my projects. I have a tendency to find ways to do things other than intended, and like to absorb the lessons.
As a result, I have quite the collection of ways not to do things, or how to do things other than I intended. Just because your management fu disagrees with my definition of productive is not my concern.
At least you're not getting reduced productivity from hn arguments online.
Brainfuck is a tiny language.
C++ is a gigantic language.
Language size and ease of use are not directly related.
As an example, Lisp has very small language size, and thus is less complex.
Casting to/from `void *` isn't needed in C, and is commonly against coding conventions, like those used by the Linux kernel.
One would generally return `NULL` instead of `0` as `void *`.
But yes, it is possible to write some funky C code that could be technically correct but still not be considered good.
libmill supports a bunch of stuff like sockets, timers, files, etc.
(Unless you need to support older Linux kernels that have epoll but no io_uring yet.)
I've used IO Completion ports on Windows before, and found it to be pretty useful API but I guess this is also a useful addition.
Basically no good idea goes uncopied by Microsoft :)
https://learn.microsoft.com/en-us/windows/win32/api/ioringap...
>Minimum supported client Windows Build 22000
Alas I have no idea from that what the lowest version of Windows that supports this API is though.