Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

After learning about effect systems, and generalising effects, my view on `setjmp` has changed considerably -- it seems effect systems "effectively" offer a design pattern for their use in languages without them.

ie., it feels that there's an async-await.h, try-catch.h, etc. to be written which would serve as c-ish design patterns. I'd be interested, then, in to what degree other langs can do the same.

As a side point, i've not yet read a good defence of why programming in C is so fun and satisfying in this way. People reduce the Rust/C issue to memory saftey... but isn't there something inherently wonderful about `while(*this++ = *that++)` (etc. etc.) ?

(A sense of fun beaten out of you by too many annotation guaranteeing its saftey?)



> inherently wonderful about `while(*this++ = *that++)`

As someone who hasn't used C as a primary language in some project for over a decade, I read this and (1) realize that LHS and RHS both post-increment, (2) don't remember if there is some UB I might be overlooking, (3) realize that the operator is assignment "=", not comparison "==", and I can't even remember what the loop termination criterion here would be. Until "*that++" is equivalent to false, or something?

It may be beautiful to the experienced programmer, but I personally would consider this just "clever" (which is a criticism, not a compliment). It feels like someone needlessly tried to pack everything into a single line of code.

This is something I'd write out more verbosely, if only to make reading it simpler. The compiler will probably generate the same machine code either way.

I'm fully aware that to the seasoned C developer, my criticism might come across as naive. However, even the fully seasoned C developer can get careless, or become tired, and in C, every one of those little things can come back and bite you in those situations.

Edit: removed the double pointer dereferencing remark, must have been an artifact from HN's special treatment of the asterisk.

Edit-Edit: I was probably wrong. I don't think it's possible to create a more verbose version without it affecting performance.


It's entirely valid C and (assuming this and that are byte pointers) copies a range of bytes until (and including) a zero byte is reached.

With a suffficient warning level (e.g. -Wall on gcc, which should always be enabled anyway, together with -Wextra), compilers will complain about the '=' and ask you to add a pair of braces to make clear that this is actually intended:

    while( (*this++ = *that++) );
It's also one of those cases where the C code matches the output assembly pretty well:

https://www.godbolt.org/z/nz1jbz4Er

As far as "obfuscated C" goes, this is a very tame example though, it's just a straightforward usage of language features, which might look strange only when coming from other languages that don't have pointers or a post-increment operator).


Doesn't that kind of prove my point?

The code as written was described as beautiful, yet would not have passed -Wall.

It's those little things that can easily get you in C, and there are so many little things to consider.


That extra pair of braces doesn't make the code 'ugly' ;)

And the code without braces is still entirely valid standard C, the warning is essentially just a lint to protect against typos (similar to JS linters warning about '===' vs '==').

PS: let's see if the alternatives would be any more readable:

    char c;
    while (c = *that++) {
        *this++ = c;
    }
...this is already buggy because it doesn't copy the final zero byte, so the test must happen inside the loop body and also lets try to get rid of the post-increment:

    while (true) {
        char c = *that;
        *this = c;
        this += 1;
        that += 1;
        if (c == 0) {
            break;
        }
    }
...hmm not really any more readable...

Let's try with an index...

    while (true) {
        char c = that[i];
        this[i] = c;
        i += 1;
        if (c == 0) {
            break;
        }
    }
...might be a bit easier to grasp when used to other languages, but readability hasn't improved all that much I'd say...

For reference, MUSL also just uses the original approach:

https://github.com/esmil/musl/blob/master/src/string/strcpy....


I would use a do-while, because at least one char is always copied:

    char c;
    do
    {
        c = *src++;
        *dst++ = c;
    }
    while (c != 0);
The post-increment is idiomatic enough in C-based languages that I wouldn’t worry about that.


I don’t write much C, but to an outsider like me this is a pretty big improvement.

It is a shame post-test loops aren’t more popular, given the similarity to the assembly they output. Seems more mechanically sympathetic. Oh well, at least it is an excuse to whip out the goto.


In Pascal, you could do

    repeat
        ...
    until c = 0;
which might be even clearer for this use case.


I was considering that but had my doubts that people who already find post-increment confusing could handle a do-while loop ;)


I find it crazy that you improve the readability so much and say readability hasn't improved that much.

The order things happen in *this++ is not obvious unless you know a bunch of C-specific rules, while the ordering of multiple statements is obvious even to someone who doesn't know C. Perhaps C programmers should find this obvious, but it seems to me more like cognitive overhead which has a non zero chance of confusing someone at some point.


That's almost a philosophical question ;) Should code in a specific language be more readable to programmers familiar to that language or to programmers who are not familiar?

E.g. I guess for a mathematician, all imperative languages are probably 'weird', while something like Haskell feels more familiar?


I was unclear, sorry: I didn't mean to say that the extra braces make it uglier, I meant to point out that something that was described as beautiful was actually flawed.

The flaw was minor in this case because the identifier names and lack of body make the intention clear, but my point is that there are a lot of minor things in C that can come and bite you at any time.

Edit: You are right, I don't see a way this could have been implemented more readable without sacrificing some performance.

First thing I thought of was:

  void cp(const char* from, char* to) {
      while (*from) {
          *to = *from;
          to++;
          from++;
      }
  }
But that does not reduce to the original case.


I've added a couple of examples trying to find a more readable version, which actually isn't trivial. Sorry for the 'post-edit' :)

As for performance: I don't think such details matter much, first, compilers are pretty good to turn "readable but inefficient" code into the same optimal output (aka "zero cost abstraction").

And a really performance-oriented strcpy() wouldn't simply copy byte by byte anyway, but try to move data in bigger chunks (like 64-bit or even SIMD registers). Whether this is then actually faster also depends on the CPU though.


`this` and `that` are arrows which range over a stream of data; `=` is copy, and `++` moves the arrow along the stream.

This isn't a "clever one-liner" it is a clear and precise syntax for expressing the operation the machine actually performs.

while(copy(current(stream_a), current(stream_b)) and not end_of_stream(stream_a))

You might prefer the above, but then, that's every other major language. The beauty of C is that the above code has to compile to something like the C version. C just allows you to actually express it


> it is a clear and precise syntax for expressing the operation the machine actually performs.

No it’s not. Your compiler will almost certainly translate this into vector instructions, at least.


For posterity, I was apparently wrong. It doesn’t autovectorize, with gcc 12.2 -O3 on godbolt, at least.


C lacks the expression of many useful common CPU capabilities. Integer rotation and overflow checking come to mind immediately.


GCC warnings can be overly pedantic. It's setup to warn about common footguns but doesn't know what your intent is. In this case it's a common enough idiom to assign within a control statement that GCC has the extra parens escape hatch.

You shouldn't just blindly let your tooling dictate how you work. It's a tool that's supposed to work for you, not control you. -Wall and -Wextra are good baselines but I always disable some of their warnings because I don't need the hassle on known good code.


Yes, with -Wall it triggers a warning, but the warning is a false positive because assignment is indeed the intention.


> the output assembly pretty well

Ironically, the compiler os likely to recognize this as a strcpy and replace it with a possibly vectorized implementation.


I actually tried to make that happen, but was unsuccessful on GCC and Clang (I've seen this in the past for mempcy() though).


Could we try to keep the topic on the article itself instead of complaints about C? It sucks to come and read the comments about this very nice article and have to scroll and scroll until I finally get to comments written by people who actually have something to say. This is a great blog, and the author puts a ton of effort into their posts. It’s hard for me not to view comments like this as being a bit thoughtless and inconsiderate.


That's why comments collapse.

There's little more useful to say here, other than I don't think most people would agree with your view of what HN is.

It's a discussion forum, not an exegetical seminar. "On topic" is whatever the topic of discussion is; and this is not constrained to the article.


Since we're already way off-topic, allow me to share my idea for solving this perennial problem: When commenting, there is a little selector:

[ ] My comment is on-topic and positive to neutral

[ ] My comment is critical

[ ] My comment is off-topic

You've got to select one. When viewing, the comment thread defaults to just showing on-topic, non-negative comments, but you can see the other stuff, too, if you want.

This solves two seemingly contradictory desires: the ability to read comments on things that interest you without having to fend off waves of negativity and wade through pools of offtopic text and the ability to speak freely.


It sounds like you're looking for an upvote counter, not a comment section.


No, that's not it at all. The point is to give a small amount of structure to the comments section, so people can see what they want.

Think of it as very roughly analoguous to the different sections of a newspaper.


That's not really how comment threads work. If you reply to a specific comment, you're replying to the stuff they said in that comment. The guy's not even complaining about c either, he's commenting on the bit of c code that the parent commenter wrote


These days the 'fashionable' way to implement async-await seems to be via compiler magic by transforming async functions into a 'switch-case state machine' and a hidden context pointer argument.

In vanilla C (without compiler magic) I've mostly seen it implemented via 'green threads' aka 'fibers' aka 'stack-switching', but TBH I'm not sure if this can be implemented with the standard setjmp/longjmp, I've mostly seen it implemented without (and instead use two small assembly functions for the context switch).

One downside of stack-switching is that it doesn't work on WASM.


The big problem with setjmp/longjmp for fibers is that a call to longjmp is undefined behavior if the `jmp_buf` argument was created by a call to `setjmp` on a different thread (1). That means fibers cannot be easily relocated onto a different thread, making M:N threading tricky to implement and erasing a lot of the benefit of fibers.

And that said, implementing a super-fast setcontext/swapcontext is like twenty lines of assembly with not too many gotchas, if you don't care about saving a few things that require syscalls.

But all that said the real downside of stack switching is that it's overkill for coroutines that can be implemented as a finite state machine, unless the runtime supports growable stacks (otherwise you pay a big cost on fiber creation, and eat a lot of memory for many fibers). There are a few languages that do this and it's super cool, but C isn't one of them.

WASM will almost certainly support stack switching, iirc there have been proposals for wasmtime to support it already?

(1) https://pubs.opengroup.org/onlinepubs/9699919799/functions/l...


> And that said, implementing a super-fast setcontext/swapcontext is like twenty lines of assembly with not too many gotchas, if you don't care about saving a few things that require syscalls.

sigaltstack(2) wasn't all that prohibitive when I did that in Python 2.6 back then. Was it 2009?

I seriously can't understand this obsession with FSMs. A naive setcontext()-based implementation outperfomed both greenlets (with its crazy legacy of memcpy-ing parts of stack from stackless) and Tornado/Twisted (with them being pure-python and therefore lacking any means to force some async on client libraries. which one does in C) while letting everyone write some nice clean synchronous-looking code.

10 years later we end up with half a language hacked up and still nowhere near the ease of use that was coded in a week or so.


> I seriously can't understand this obsession with FSM

It's the most optimal representation of the common-case (non-recursive asynchronous tasks), has the same overhead as a function call, plays very well with branch prediction, can be easily inlined by optimizers, and it's a lot easier to implement.


I wrote an unrolled switch statement in Java to simulate eager async/await across threads.

https://github.com/samsquire/multiversion-concurrency-contro...

The goal is that a compiler should generate this for you. This code is equivalent to the following:

   task1:
     while True:
       handle1 = async task2();
       handle2 = async task3();
       print(await handle1)
       print(await handle2)

   task2:
     n = 0
     while True:
       yield n++

   task3:
     n = 0
     while True:
       yield n++
It doesn't actually run task2 and task3 both eagerly, I've not got around to scheduling the tasks on DIFFERENT threads. They currently queue onto a single thread, so task2 and task3 are parallel to task1 (but maybe not at the same time) but task2 and task3 are not parallel to each other. This is my goal.

I also ported the coroutines in this blog post https://blog.dziban.net/coroutines/ to GNU Assembler https://github.com/samsquire/assembly/blob/main/coroutines.S

This might be useful to someone who wants to port this to C. This uses the stack switching idea. So they are stackful coroutines.

There's also Tina a header only coroutine library https://slembcke.github.io/Tina

I also played with protothreads http://dunkels.com/adam/pt/

I also asked in a stackoverflow post how to use a thread pool with C++20 coroutines: https://stackoverflow.com/questions/74520133/how-can-i-pass-... Someone provided some example code, which seems to work with C++ coroutines :-)


stack switching with setjmp/longjmp can be implemented like this: https://stackoverflow.com/a/8817009/1116739

But it's messy enough that you'd want a library/framework to help you handle it.


POSIX pre-2008 had (and Linux/Glibc and {Free,Net,DragonFly}BSD still have) <ucontext.h> with proper stack switching functions, used as a fallback in a number of coroutine libraries. The fallback status is due to a self-inflicted inefficiency: they save and restore the signal mask, thus still need to go through the kernel (why e.g. Linux does not put signal mask manipulation in the vDSO, I don’t know). POSIX yanked them and now recommends rewriting to use POSIX threads instead, which is asinine.


And the 2008 was about the time that coroutines were gaining popularity again!


Putting each tasklet's stack in different places on the actual stack and jumping between them is inherently unsafe and not portable. You must be sure that each tasklet does not consume too much stack so that it does not overwrite another.

On BSD Unices, you can only longjump back up the stack. Otherwise, longjmp() will call longjmperror() and terminate the program.


> Putting each tasklet's stack in different places on the actual stack and jumping between them is inherently unsafe and not portable. You must be sure that each tasklet does not consume too much stack so that it does not overwrite another.

It's absolutely unsafe and a ridiculous to do, but what's the reasoning for it being unportable? Wouldn't it just be just as unsafe anywhere that C compiles?

> On BSD Unices, you can only longjump back up the stack. Otherwise, longjmp() will call longjmperror() and terminate the program.

The manpage claims that the semantics is not "only jump back up the stack", but rather that you can't longjmp to "[...] an environment that that has already returned". Technically, the tasklet the we're longjmp'ing to never terminates, right?


In any case, you definitely can't do it on Windows and Emscripten (WebAssembly), where longjmp invokes the same stack unwinding behavior as C++ exception handling, rather than just setting some registers and jumping. Windows has its own APIs for tasklets (fibers); no such luck on Emscripten.


I once took an "Advanced C Programming" class (which would have been better named as "How to do OOP in a language never intended for it") where the instructor expressly prohibited the use of "*i++" and several other language elements because he thought they were confusing. I got into many arguments with the instructor throughout the course, and I figured I would get a poor grade, but he still gave me an A. My main disagreement with him was this: If the language elements are there and well defined, why prohibit their use? The course after all was "Advanced C", wasn't it?


That has been my argument for "preprocessor abuse" - there isn't such thing as preprocessor "abuse"[0], it is part of the language and provides some form of extensibility in a language with an already limited set of features.

If anything the preprocessor needs more features (let me include files from macros and do loops dammit :-P).

[0] ok, i can think of some uses that might count, like "#define BEGIN {", etc that serve no practical purpose, but i don't think anyone called these "preprocessor abuse".


You can have all that with m4. It's a perfectly suitable C preprocessor.


m4 is a bit weird but if you can use that you can use any preprocessor - including a custom one.

The issue is that chances are said preprocessor wont work with editors and IDEs that can parse C to provide tools like syntax completion, jumping to definitions, etc - if anything you'd be lucky if you get working line numbers t use a debugger with.


"Programs must be written for people to read, and only incidentally for machines to execute"

-- Abelson & Sussman, Structure and Interpretation of Computer Programs, 1984.

It's possible to write English with bad structure, clumsy metaphors, obscure vocabulary, and non-non-non-usual idiosyncrasies. And other people might be technically capable of understanding it if they really want to, and try hard enough.

After all, the language elements are there and well-defined, so why would anyone ever complain about "bad" writing?

(Out of curiosity, do you object to people who say the use of "goto" should be seriously restricted, or even prohibited, in most programs? Do you specifically use gotos to make the point that they can still be be useful and productive? The language element is there and well-defined.)


I've used a goto in production code twice in >30 years. In both cases it was the right thing to do, and it had more to do with hardware elements and mission assurance than with software engineering.


> isn't there something inherently wonderful about `while(this++ = that++)` (etc. etc.) ?

There is something inherently wonderful about riding a motorcycle without a helmet too. That doesn't mean it is wise.


You might be interested in libhandler:

https://github.com/koka-lang/libhandler

Basically an effect handling library for C written by the folks developing the koka language.


About your last two questions yes and yes.

I love C.




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

Search: