Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Show HN: A pure C89 implementation of Go channels, with blocking selects (github.com/rochus-keller)
271 points by Rochus on Dec 13, 2023 | hide | past | favorite | 166 comments


1. There is no <memory.h> in any version of ISO C, nor in POSIX.

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.


One more attempt, so that children can understand it too:

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.


Wow you could have said that without being incredibly condescending.


I could also have done something else with my free time than solve this problem and then let a bunch of nitpickers reproach me with nonsense. See e.g. this comment which was one of the first and stayed on top for a day: https://news.ycombinator.com/item?id=38633438


Nit picking is indeed annoying. In regards to efficiency however, I’m of the opinion that ignoring nit pick or venomous comments and instead positively engaging with other folks in the thread who have useful comments and feedback would be more effective.

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.


Thank you for your words.

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.


"Pure" doesn't apply to "language purity" (nobody besides a very few seem to really care about the latter, especially not in C, not even compiler authors), but that nothing else is required than a C89 compatible toolchain to use the library (i.e. it doesn't need any assembler or third-party tools/libraries). See also https://www.merriam-webster.com/dictionary/pure?utm_campaign.... And so far the library compiles with all of my C89 copilers, even the very old ones (and old Posix versions). For a few cases to check, there are notes in the source code.


> nothing else is required than a C89 compatible toolchain

Fair enough. A "pure C89" program should be expected to work in Turbo C on MS-DOS.


No C89 compatible toolchain does "pure C89" without extra options (if at all); people would run away. Even if you use GCC or CLANG with the -std=c89 option it doesn't. So the complaints that the presented library is not "pure C89" is nothing but an academic quibble.

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.


There are C89 toolchains that will not translate and execute the code, no matter what options they are given to control the language dialect, or anything else.

> 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.


Again: 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.

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.


I was being helpful by pointing out that the code might not reflect the author's intent.

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.)


Man, you didn't do a s**t. Just spending five minutes with the code reveals that it is compiled with -std=c89 and strict adherence to a standard is neither a promise nor a goal of the author. "C89" is not a protected term and is not uniformly understood or used in any way. So don't be so picky.


There isn't a nanometer of wiggle room in what C89 refers to.


Ha ha, dream on, my friend ...


There is a difference between words and jargon terms.

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.


Wow, thank you for this. Good work!

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.


> 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?

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.


> a spin lock with exponential backoff

Also measure before you implement this yourself, to my knowledge this is infact what the linux mutex does anyway.


Very interesting! I program a lot in Go, and while I have my complaints about the language, I find the CSP model to be very easy to work with.

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!


What are your complaints about the Go language that is better in C89?


libmill (https://github.com/sustrik/libmill) and libdill (https://github.com/sustrik/libdill) should be similar and probably mentioned.

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?


Was going to ask if the current implementation could be used together with libmill, but maybe they overlap a lot in functionality?


I don't think you can. The library would block the libill worker thread, which would also block all other libmill coroutines from running. The libmill channels need to yield in a way where they don't block the underlying threads.


Basically what ended up replacing Alef in Plan 9.

https://9p.io/magic/man2html/2/thread


Well, both use channels, as did Occam, Joyce or Newsqueak even before. But my library uses kernel threads (Pthreads, Win32 threads), no coroutines, nor user mode scheduling. So there is little in common with Alef or Plan 9. But there are indeed C libraries which do that; mine has other goals.


This is really cool!

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.

[0] http://czmq.zeromq.org/


Depending on your use case, zmq inproc I've found has a lot more latency than you would expect. like p50 of >100us and p99 can be over 1ms.

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)


while (!CspChan_closed(chan)) { ... } seems like a concurrency footgun – what's stopping the channel from being closed in between the check for it being closed and the operation on the channel?


Nothing stops that. You'll end up getting a message full of 0 bytes (messages are fixed size) if it closes between the `closed()` check and the `receive()` operation. Common designs in this space instead indicate if the receive call has obtained data (return values typically indicate this). This is a kind of time-of-check/time-of-use bug.

Go, for its part, has its receive operation, `<-`, return a status indicating if the channel is still open, as in `v, ok := <-ch`


I don't think CspChan_closed is a super useful operation if using it is always prone to TOCTTOU issues – maybe as an optimization hint but that's about it. In Go, the receive operation indicating the channel is closed is atomic with receiving a value, so it's not possible to have a TOCTTOU problem there.


And further, what if there are messages still in the closed channel? Do they just go "poof"? It's fine if they do, but that should be documented.


It's 2023 - even MSVC has supported C11 and C17 for a while now. C89 is no longer a feature to advertise, it's an unhelpful constraint forcing poorer quality code. A good example is that CspChan_closed should use the bool type added in C99.

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]


I realize your intention is to provide good feedback and you obviously know a lot, but if you could shift the pH slightly away from acidic, that would be better. Keep in mind that criticisms like this have a tendency to land 10x harder than you intended.

https://news.ycombinator.com/showhn.html


This has to be the best comment from a moderator that I've ever seen. This is why I love HN.


I almost never agree with mods here, but despite that I’ve got to admit that the way the expose their opinions is one of the best Ive experienced


Just a note, because this is a common misconception that many folks bring from Reddit:

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.


Sigh. Yes. Valid. I'll go edit it a bit.


And what we're looking at now is the non-acid version? You seem to have completely missed that the person was implementing something interesting. Here's something for you to do: think about whether, at work, you're good at giving credit to other people publicly. How do you think people feel on reading your code review comments? More junior employees? Are you happy with this?


The edit moved in a good direction and that's worth reinforcing. It's not worth it to pile on further—it risks shaming, which would produce a backlash, which wouldn't benefit anyone.


Thank you dang very much for that explanation. It is a good example of social interaction and of second order effects. You are setting a good role model.


Don't want to start a thread and agree with what dang has said but I do want to give some perspective from my side.

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.


Comment moved to https://pastebin.com/NgJ7VHSU due to length and off-topic-ness.


Appreciated!


The other reason to love this place is OPs take rebuke with grace. Kudos to you sir.


[flagged]


If that was sarcasm, I don't think it landed. If not, well, let's not discourage people from admitting when they are wrong. It's a healthy thing to do.


How much do you get paid to be mod?


Undisclosed: https://www.newyorker.com/news/letter-from-silicon-valley/th...

.. but likely more than either of us get paid to comment.


> should really be a bool

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.


> 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.

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.


> The return type of comparison operators is entirely irrelevant

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.


When folks talk about namespaces, structs, & typedefs in C, it's to call out that `struct X` and `typedef int X;` can both exist at the same time. In other words, nothing prevents one from doing a `typedef struct X { int f; } X;` or similar, using `X` as the name in both places.

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.


> more correct to say "struct names have their own namespace"

That is false. If we declare an "enum foo", which is not a struct, then we cannot have a "struct foo" in that scope.


Sure, you're right, it's just not really relevant here.


Well, your two comments set a standard for relevance that is not easily attained by others.


> 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.

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.


What about size of stored bool variable? Just using int will take 4 bytes (Or something else, but still bigger then 1), while stdbool will only use 1 byte, no?


In arrays, you can use char. In structs, there are also bitfields like unsigned flag : 1.


It's 2023, and I love this. There is no reason to criticize the mere fact that someone took the effort to make something that portable.

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.


C89 sometimes actually means gnu89 which is decidedly non-portable to modern standards in subtle ways. It's time to put that horse to pasture and consider at least using a 24 year old standard instead.


If gnu89 is not a perfect c89, so what? c11 is also not a perfect c89. ruby is also not a perfect c89. Swift is more convenient than c89 for ios. c89 doesn't have tons of features that c# has. We could say true yet irrelevant things all day long. You don't like c89 flavor ice cream, got it. So what?


The only thing that matters here rather than the nonsense hypotheticals is that they are right, the library isn’t C89 as pointed out by others. This isn’t about not liking C89 flavor ice cream, but calling it C89 when it isn’t. Like, of all things why mislead about that? Very bizarre behavior from the OP and these comments.

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.


Also, as someone who wrote code that had to support C89-only compilers as recently as 5 years ago, I noticed immediately that this code mixes code and variable declarations which strictly C89 compatible compilers will barf on. That said, this code also uses pthreads which is not exactly as portable as "pure C89" implies either.


> that's just the header

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 :(


btw, just to note, both your and dang's comment were very appropriate in calling me out for the very poor form of the original comment. Either of your comments would've reminded me that I don't normally do that, and I would've gone back and edited it. I replied to dang's comment (the "Sigh. Yes. Valid.") because I saw that first, but I also appreciate your pointing out the same thing, and in particular that you did so without reacting to my bile with more bile yet.

Thanks!


No worries!


What's unfriendly in their tone?


They edited their original comment before you saw it, apparently the original tone was a bit harsh


> It's 2023 - even MSVC has supported C11 and C17 for a while now. C89 is no longer a feature to advertise

There are plenty of constrained environments where this is indeed a feature to advertise, namely older machines and embedded systems.


But it's not actually written in C89. It uses POSIX threads, flexible array members, anonymous unions, bitfield members that are not int, declarations intermixed with statements. And that's just what “gcc -std=c89 -pedantic-errors” reports.


> older machines

Anything from the 80s/90s isn't really worth supporting. We gotta draw the line somewhere.


Ok, I agree that these points might have been made more constructively, but ...

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.


I would note that Tiny C Compiler is used a fair amount in the wild today, even beyond its stated use cases on the landing page, and C89 is the only standard that it fully supports.


According to the README it supports most of C99, and even some things beyond that. It's not clearly defined what's meant with "most" or what's not supported, but since I've never had any problems with any C99 code I've written I suspect it's mostly fairly little used stuff (complex numbers?), and it seems that for all practical purposes tcc supports C99 for almost all codebases.


Out of curiosity where is tinycc still being used?


Fun stuff like running OS's in your browser:

https://bellard.org/jslinux/

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.


Languages that transpile to C code like it because, if the C is generated by another compiler, then the C language's expressiveness doesn't really matter, and TCC builds machine code pretty fast.


With big code bases, to get fast feedback. For me it's 200ms vs 3min


C89 is a requirement where I work still, so C89 libraries are useful here. We support vendor C toolchains that don't always have C99 features.


MSVC lacks some C99 features like variable length arrays and complex numbers. There's always a work around, but wouldn't want to use that compiler for C unless I had to.

Going for C89 for a truly portable project is probably fine.


MSVC added that a few years ago. It took them a (very) long time, but it should now be fully C99-compliant, even C17 I think (either fully or mostly).


No. That syntax is not supported. Try a VLA and see how it goes. Then try a complex number or read the docs: "The compiler doesn't directly support a complex or _Complex keyword" - https://learn.microsoft.com/en-us/cpp/c-runtime-library/comp...


Supporting C11 and C17 by definition doesn't require supporting C99 features made optional in C11.


No good deed goes unpunished.


Amen.


> edited to tone down a bit

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.


>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?

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."


For me C89 means 'will build anywhere' whereas C99+ means 'may or may not build anywhere', rather than a value judgement about the author. But my background was in firmware where sometimes you were using custom compilers or very old compilers.


As someone whose idea of fun is trying to compile open source software on OS X / PowerPC, portability has a special place in my heart. While I view open source software as a "gift to the world", C89 + MIT is at the far end of the spectrum of how generous that gift is.

https://imgflip.com/i/89b05r


It's a library, one of the goals of which is compatibility with the many C89 compilers still in use today, so how else should I name it? And why is it bad or provoking to have a gray beard (which sounds a lot like age discrimination to me) or to highlight a "serious hacker aspect"?


> It's a library […] C89 […] how else should I name 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.


> replace "pure C89" with "C89 compatible"

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.


These aren't really my judgements, I'm just guessing why you got some criticisms.

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.


> I feel like purposely emphasizing C89 in your project name is meant to highlight some sort of greybeard or serious hacker aspect to it.

Or indicating that it will work if you want to fold the code into a C89 project, which is not exactly a rare concern.


What's the name of the book?


"Parkinson's Law And Other Studies in Administration" - a very enlightening book that has lost none of its relevance.


Oh shit it me. Thank you! Time for me to reflect on this for a bit…


Not dismissing anything you said but bool isn't C89 right ? Back ~6 years ago when I worked in embedded C89, bool was just a #define true (1==1), #define false (0==1) so I guess it makes sense it isn't in the lib ?


Indeed, bool (or _Bool) was added in C99, hence my pointing out it not being used… it's not in C89.

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.


Ah, I get it now, thx.


> It's 2023 - even MSVC has supported C11 and C17 for a while now. C89 is no longer a feature to advertise, it's an unhelpful constraint forcing poorer quality code.

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


Perhaps I'm readint it wrong bu this comment is crticising someone for not using added language features. Maybe the author does not need them. "YAGNI". IMO, it should be the author's choice whether or not to utilise certain features. As someone else noted in another comment, C89 is more portable than C11 or C17. To me, C89 is like Bourne shell. An author can choose to write scripts in Bash but they will not be as portable as ones written in Dash, nor as fast. The only constraint I have run into with C89, when compiling lex.yy.c, is that it has a limit of 32767 lines.


IIRC, reservation of _t in typedefs is for the POSIX rather than C standards.


The standards extensions reservation of _t is such a non-issue I don't understand why people even bother mentioning it anymore.

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.


Do you know of a good style guide that encapsulates common conventions like create/destroy?


Even C has already a syntax way too rich and complex (and C11 and C17 tantrums are making things worse).

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?


What's wrong with ternary operator ("a?b:c") ? It seems many people hate it but I've never seen a reason for it.

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");


I (a different person replying to your post) don't mind ternary operators, but I do prefer if-expressions which convey the same thing semantically but can be nicer to read (and also way better when chaining else-if ladders - nested ternaries can be a nightmare to read). https://stackoverflow.com/a/46843369


To me personally, the use case for ?: is selecting a value for a const:

    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.


I was confused by your reply when I first read it (didn't quite understand it), but I think it comes from a miscommunication when I had intended if-expressions (if statements that return a value) while you had if-statements in mind.

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.


Sorry for the confusion, but you did indeed decipher my meaning (if-statements)! I agree that if-expressions are far more readable in languages that support them (Python and Haskell come to mind for me), even though they're arguably just syntactic sugar for the ternary operator.


I would say that languages where "if" can be used as expression provide a different (perhaps better) syntax for the same ternary operator.

    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 :-)


It's actually

    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.

https://docs.raku.org/language/operators#infix_??_!!


You are right, of course. Sorry! "Colon belongs to Larry" :-)

I think I made this mistake because :: was used in one of the drafts for Perl 6, but the final specification uses !!


In that case you can have the result of a function call be assigned to the const, and the function be the if/else for the value. With a well-named function it can even be more readable than the ternary.


Having to create a function where a simple expression would suffice is "typing in mittens" programming...


Fair enough.. I actually use ternary sometimes, especially in brief return statements, and especially when both results are simple values. I only really avoid it when the result expressions are multiline. I just felt like pointing out the obvious when I saw GP's comment.


Chained ternaries are quite readable when formatted as tables...

    a = (x < 5 ) ? 'LESS'
      : (x > 5 ) ? 'MORE'
      :            'SAME';


I don’t like them because they’re easy to write and a pain to deal with later. My biggest irritation comes when I’d like to break inside the if or else clause of a ternary and can’t. There is also more mental overhead to parse and keep in the mental stack.

I have also seen them used cleanly and beautifully, but that is a rare occurrence.


Something which is significantly "controversial", or has no significant and pertinent impact, should not increase the compiler technical cost. Syntax sugar is the enemy of compiler technical costs.

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.


What you mean by "breaking inside"? You cannot nest ternaries, but you can chain them (see my other comment above).


I like the ternary too but to pretend that it's ok because it's useful in perl is more of a counter argument...


A was not using Perl as an argument, I am just much more familiar with Perl than C, so I wondered if the ternary operator works differently in C than in Perl, making it more problematic/less useful.


> Namely it is easier to write a naive rust compiler than a C compiler?

You must be joking.


That's a question.

You know the answer?


Writing a Rust lexer/parser, maybe. The entire compiler? Dubious.


"maybe", then you don't know if it is easier to write a rust naive compiler than a C naive compiler (look at tinycc, cproc/qbe, simple-cc/qbe, etc).


You don't seem to be asking a question anymore. That's not what I said maybe to, anyways. If both languages have grammars that are on the same level, I don't see why someone experienced in writing a lexer/parser would have more difficulty with one. I think Rust only needs lookahead for raw strings and there are some quirks with C's grammar, but I'm not knowledgeable about that.


Easier to write a rust compiler than a C compiler? I must be somehow misinterpreting this.


It is a question.


I feel like this comment could be best structured in the form of a series of commits sent as a combined Merge Request.

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.


Sorry for going a bit off-topic, but something I'm curious about: are channels thought to be a good/useful tool for concurrent programming?

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?


I program in Go a lot professionally. I use channels a decent amount of the time, but often drop down to using mutexes or atomics. But I generally prefer channels to mutexes and atomics; generally I reach for those alternatives only due to performance concerns.

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.


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).

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).


> 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’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.


> 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.

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!


The opposite. Channels are often used to orchestrate higher-level structures/abstractions; not 'low-level' stuff where more common primitives such as mutexes/semaphores are sometime preferred.


Even looking at simple code example, with casting from/to void, returning 0 as void etc, I cannot understand how people are calling C easy and productive language


Despite all of the scary casting, there is fairly little you need to know about C to understand what's going on.


You're making bits dance through hardware with minimal overhead or layers of abstraction/other programmer's opinions to deal with.

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.


Having your own definition of productivity means no one can argue or have a discussion about productivity with you.

At least you're not getting reduced productivity from hn arguments online.


That claim isn't made on the linked site, though. I think people who write C nowadays tend to be working in the drivers and OS development spaces.


There's at least 4 kinds of C programmers today:

- 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 think people who write C nowadays tend to be working in the drivers and OS development spaces.

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.


Perhaps not the first language. In my experience C is often in the second batch of languages after the introductory one (java back in my day).


The things you've described are either not required or often considered bad style in C even though they are valid C.

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.


C is a small language. It's not simple, nor is it easy.

Brainfuck is a tiny language.

C++ is a gigantic language.

Language size and ease of use are not directly related.


Language size can imply complexity.

As an example, Lisp has very small language size, and thus is less complex.


Only if by Lisp you mean Lisp 1.5 language report.


Related is libmill, which has been around for awhile and is was previously discussed on HN: https://news.ycombinator.com/item?id=30699829

libmill supports a bunch of stuff like sockets, timers, files, etc.


Domains are expired. Do you have other links?


I love seeing projects like this, thanks for sharing. I've worked in constrained environments before where anything past C89 was trouble, it's a nice target for portability even if it takes more work.


When it said pure C89 I figured it was going to have some setjmp and longjmp going on instead of threads.


fun fact - Go Channels started out as pretty thin syntax sugar over features provided by Plan9's C libthread.


if you don't get anything out of this and don't know Hoare's work, go read CSP - http://www.usingcsp.com/cspbook.pdf


not being a god level programmer i wont go into quality of code, but this looks to me really neat and easy to use. well done!


Great! I was looking into something like this. I assume ending up with epoll will be better?


Considering that epoll is Linux specific anyway, I would highly advise going straight to io_uring. epoll has a whole bunch of footguns in particular with edge triggered modes of operation; io_uring has a higher initial threshold in understanding how it works but is worth that effort.

(Unless you need to support older Linux kernels that have epoll but no io_uring yet.)


Oh yeah I meant io_uring too. Plus Windows copied it so you can implement things very similarly for Windows.


Thanks for that, had no idea.

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.


Build 22000 is Windows 10 21H2.


Is there something similar on macOS/iOS?


Kqueue! Not the same design or as flexible as io_uring though.


Thanks for this. It would actually be nice to have a plain old Makefile. The .pro file doesn't look like anything I know.


Welcome; I don't use make; the pro is a qmake file; I likely will add a BUSY file, but until then you can just use e.g. "cc -std=c89 *.c -lpthread".


Brilliant. Thank you so much!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: