Coroutines in C (3/3)

All this talk about coroutines and bees is nice but how do they work? What's under the hood?

As mentioned in the previous posts, the implementation is in a single header of less than 70 lines. Let's have a look at it piece-wise (I suggest you open the file from Gituhub in another window).

When we define a bee:

beedef(iter, int n;)
{
  ... // code to be executed here.
  beereturn;
}

what really happens is that:

  • a new type iter (a pointer to a structure) is defined.
  • a function bee_fly_iter() is defined.

Here is the code above after the preprocessor magic:

typedef struct iter_s {
   struct bee_s bee_;
   int n; // <- This is the preserved var
 } *iter; 

 int bee_fly_iter(iter bee)
 { 
   if (bee == NULL) goto bee_return;
   switch(bee->bee_.line) {
       default: goto bee_return;
       case  0: {
          ... // code to be executed here.
       } 
   } 
   bee_return: bee->bee_.line = -1; 
   return BEE_DONE;
 }

Seems a pretty normal looking code, right?
The structure bee_s holds the basic information:

  • which function to call when we want the bee to fly
  • which line the bee should resume its work
typedef struct bee_s {
  int32_t line;
  int (*fly)();
} *bee_t;

When a bee is created, the value for line is set to 0, so the first time the fly function is called the switch will execute the code following case 0:.

When yelding, the value for line will be set to the current line number and a new case entry is created as you can see from the beeyeld definition:

#define beeyeld  do { \
                   bee->bee_.line = __LINE__ ;  \
                   return BEE_READY; \
                   case __LINE__ : ; \
                 } while(0)

By yelding, the the fly function returns to the caller and next time it is called, the switch will start the execution from the case __LINE__: statement.

I believe that comparing the code before and after the macro expansions will clarify things better:

1: #include "bee.h"
 2: 
 3: beedef(iter, int n;)
 4: {
 5:  for (bee->n = 0; bee->n < 10; bee->n++) {
 6:    beeyeld;
 7:  }
 8:  beereturn;
 9: }
10:
11: int main(int argc, char *argv[])
12: {
13:    iter counter = beenew(iter);
14:    while (beefly(counter)) {
15:      printf("%d\n",counter->n); 
16:    }
17: }

becomes:

typedef struct iter_s {
   struct bee_s bee_;
   int n;
} *iter;

int bee_fly_iter(iter bee)
{
  if (bee == ((void *)0)) goto bee_return;

  switch(bee->bee_.line) {
    default: goto bee_return;

    case 0: {
            for (bee->n = 0; bee->n < 10; bee->n++) {
              do {
                bee->bee_.line = 6;
                return 1;
    case 6: ;
              } while(0);
            }
    }
  }
  bee_return: bee->bee_.line = -1;
  return 0;
}

int main(int argc, char *argv[])
{
    iter counter = bee_new(sizeof(struct iter_s), bee_fly_iter);
    while (beefly(counter)) {
      printf("%d\n",counter->n);
    }
}

You may notice that 6 is the line where beeyeld appears.
The Duff's device appears here in the form of a for loop nested in a switch statement. The evil is that when resuming, we'll jump straight into the loop body.

Note that for how much convoluted this may seem, there's no undefined beheviour or dependency on a specific compiler involved. Everything is played according the C standard rules.

I believe that once the basic concepts are understood, the rest is pretty simple to get.

As I said, this is just the basic mechanism, more work is needed to use bees within your project. Nevertheless I believe this is a nice tool to have at your disposal and I hope you'll find it useful.

Don't hesitate to ask and provide feedback, I'll be very happy to respond.

16