[HN Gopher] Show HN: A pure C89 implementation of Go channels, w... ___________________________________________________________________ Show HN: A pure C89 implementation of Go channels, with blocking selects Author : Rochus Score : 114 points Date : 2023-12-13 19:31 UTC (3 hours ago) (HTM) web link (github.com) (TXT) w3m dump (github.com) | sim7c00 wrote: | 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! | eqvinox wrote: | 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] | coumbaya wrote: | 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 ? | eqvinox wrote: | 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. | coumbaya wrote: | Ah, I get it now, thx. | dang wrote: | 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 | eqvinox wrote: | Sigh. Yes. Valid. I'll go edit it a bit. | dang wrote: | Appreciated! | drewg123 wrote: | This has to be the best comment from a moderator that I've | ever seen. This is why I love HN. | lnxg33k1 wrote: | 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 | oconnor663 wrote: | > 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 :( | wiseowise wrote: | What's unfriendly in their tone? | kazinator wrote: | > 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. | eqvinox wrote: | > 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. | kazinator wrote: | > _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. | sylware wrote: | 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? | IlliOnato wrote: | 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"); | zogrodea wrote: | 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 | dgfitz wrote: | 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. | MrRadar wrote: | 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. | tomcam wrote: | > 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. | 38 wrote: | > older machines | | Anything from the 80s/90s isn't really worth supporting. We | gotta draw the line somewhere. | fweimer wrote: | 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. | pengaru wrote: | 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. | neverartful wrote: | No good deed goes unpunished. | Galanwe wrote: | > 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 | dekhn wrote: | 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. | jansommer wrote: | 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. | vore wrote: | 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? | withinboredom wrote: | 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. | rockwotj wrote: | 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. | pjmlp wrote: | Basically what ended up replacing Alef in Plan 9. | | https://9p.io/magic/man2html/2/thread | bufo wrote: | Great! I was looking into something like this. I assume ending up | with epoll will be better? | eqvinox wrote: | 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.) | bufo wrote: | Oh yeah I meant io_uring too. Plus Windows copied it so you | can implement things very similarly for Windows. | mmcgaha wrote: | When it said pure C89 I figured it was going to have some setjmp | and longjmp going on instead of threads. | samsquire wrote: | 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. | galkk wrote: | 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 | vngzs wrote: | 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. | salawat wrote: | 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. | zeroCalories wrote: | Despite all of the scary casting, there is fairly little you | need to know about C to understand what's going on. | SAI_Peregrinus wrote: | 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. | p_l wrote: | fun fact - Go Channels started out as pretty thin syntax sugar | over features provided by Plan9's C libthread. | Zambyte wrote: | 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/ | Matthias247 wrote: | 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? | segmondy wrote: | 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 | openasocket wrote: | 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! | iainmerrick wrote: | 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? | JyB wrote: | 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. | kazinator wrote: | 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. ___________________________________________________________________ (page generated 2023-12-13 23:00 UTC)