from Hacker News

Hacking Coroutines into C

by jmillikin on 7/13/25, 1:12 AM with 42 comments

  • by adinisom on 7/13/25, 5:04 AM

    My favorite trick in C is a light-weight Protothreads implemented in-place without dependencies. Looks something like this for a hypothetical blinky coroutine:

      typedef struct blinky_state {
        size_t pc;
        uint64_t timer;
        ... variables that need to live across YIELDs ...
      } blinky_state_t;
      
      blinky_state_t blinky_state;
      
      #define YIELD() s->pc = __LINE__; return; case __LINE__:;
      void blinky(void) {
        blinky_state_t *s = &blinky_state;
        uint64_t now = get_ticks();
        
        switch(s->pc) {
          while(true) {
            turn_on_LED();
            s->timer = now;
            while( now - s->timer < 1000 ) { YIELD(); }
            
            turn_off_LED();
            s->timer = now;
            while( now - s->timer < 1000 ) { YIELD(); }
          }
        }
      }
      #undef YIELD
    
    Can, of course, abstract the delay code into it's own coroutine.

    Your company is probably using hardware containing code I've written like this.

    What's especially nice that I miss in other languages with async/await is ability to mix declarative and procedural code. Code you write before the switch(s->pc) statement gets run on every call to the function. Can put code you want to be declarative, like updating "now" in the code above, or if I have streaming code it's a great place to copy data.

  • by astrobe_ on 7/13/25, 8:02 AM

    [State machines] lacked a linear flow

    That's because you need a state machine when your control flow is not linear. They are represented by graphs, remember? This is actually a case where using gotos might be clearer. Although not drastically better because the main problem is that written source code is linear by nature. A graph described by a dedicated DSL such as GraphViz has the same problem, although at least you can visualize the result.

    But control flow is only one term of the equation, the other being concurrency. One typically has more than one state machine running; sometimes one use state machines that are actually essentially linear because of that. Cooperative multitasking. I would question trying to solve these two problems, non-linearity and concurrency. Sometimes when you try too hard to kill two birds with one stone you end up with one dead bird and a broken window.

    One lecturer of the conference announced earlier [1] made that point too that visualization helps a lot, and that reminded me of Pharo's inspection tools [2]. Seeing what's going on under the hood is more important that one usually thinks.

    One issue with state machines is that they are hardly modular: adding a state or decomposing a state into multiple states is more work than one would like it to be. It is the inverse problem of visualization: what you draw is what you code. A good tool for that would let the user connect nodes with arrows and assign code to nodes and/or arrows; it would translate this into some textual intermediate language to play nice with Git, and a compiler would transform it to C code for integration in the build system.

    [1] https://bettersoftwareconference.com/ [2] https://pharo.org/features

  • by mikepurvis on 7/13/25, 1:32 AM

    FreeRTOS can also be used with a cooperative scheduler: https://www.freertos.org/Why-FreeRTOS/Features-and-demos/RAM...

    That said, if I was stuck rolling this myself, I think I’d prefer to try to do it with “real” codegen than macros. If nothing else it would give the ability to do things like blocks and correctness checks, and you’d get much more readable resulting source when it came to stepping through it with a debugger.

  • by userbinator on 7/13/25, 4:23 AM

    Of course, the project didn’t allow us to use an RTOS.

    That tends to just make the project eventually implement an approximation of one... as what appears to have happened here.

    How I'd solve the given problem is by using the PWM peripheral (or timer interrupts if no PWM peripheral exists) and pin change interrupts, with the CPU halted nearly 100% of the time. I suspect that approach is even simpler than what's shown here.

  • by syncurrent on 7/13/25, 7:27 AM

    A similar approach, but rooted in the idea of synchronous languages like Esterel or Blech:

    https://github.com/frameworklabs/proto_activities

  • by Neywiny on 7/13/25, 1:34 AM

    The intent here is nice. I historically hate state machines for sequential executioners. To me they make sense in FPGA/ASIC/circuits. In software, they just get so complicated. I've even seen state managers managing an abstracted state machine implementing a custom device to do what's ultimately very sequential work.

    It's my same argument that there should be no maximum number of lines to a function. Sometimes, you just need to do a lot of work. I comment the code blocks, maybe with steps/parts, but there's no point in making a function that's only called in one place.

    But anything is better than one person I met who somehow was programming without knowing how to define their own functions. Gross

  • by user____name on 7/13/25, 11:25 AM

    I've recently read a bunch of articles explaining these weird macro soup setups for emulating coroutines in C. This one is probably the most advanced writeup in implementing fibers/coroutines I came across. The focus is on a multithreaded context, which seems to complicate things a lot. Honestly I feel like you need language level support for them in that case, they seem more trouble than they're worth otherwise, at least in plain C.

    https://graphitemaster.github.io/fibers/

  • by jonhohle on 7/13/25, 7:17 PM

    I’ve used libaco for coroutones in C and liked it. In my case I used it to deal with the differences betwen between eventing in libevent/libuv and feeding zlib for streaming decompression. It allowed the zlib loop to continue to look like a standard zlib loop.
  • by throwaway81523 on 7/13/25, 1:40 AM

    As the article acknowledges at the end, this is sort of like protothreads which has been around for ages. The article's CSS was so awful that I didn't read anything except the last paragraph, which seemed to tell me what I wanted to know.
  • by codr7 on 7/13/25, 1:49 PM

    Looks overly complicated to me.

    This is an alternative I wrote for my C book:

    https://github.com/codr7/hacktical-c/tree/main/task

  • by moconnor on 7/13/25, 8:04 AM

    A colleague of mine did this much more elegantly by manually updating the stack and jmping. This was a couple of decades ago and afaik the code is still in use in supercomputing centres today.
  • by Asooka on 7/13/25, 6:40 PM

    I would have used CPC or adapted qemu's coroutines. Coroutines in C are a very well explored field with several mature solutions.
  • by Nursie on 7/13/25, 5:41 AM

    Cooperative multithreading via setjmp and longjmp has been around in C since the 80s at least.

    I’m not sure this is so much hacking as an accepted technique from the old-old days which has somewhat fallen out of favour, especially as C is falling a little outside of the mainstream these days.

    Perhaps it’s almost becoming lost knowledge :)

  • by webdevver on 7/13/25, 9:16 PM

    the chiark green end guy has an article about this topic too
  • by Agyemang on 7/13/25, 9:56 AM

    Yes
  • by joshlk on 7/13/25, 12:06 PM

    Rust can be used in an embedded environment and also offers asynchronous execution built into the language