Skip to content

Refactor the convention for coroutines #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tinync/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ tinync: tinync.o
tinync.o : tinync.c

clean:
rm -f tinync tinync
rm -f tinync.o tinync

.PHONY: all clean
85 changes: 85 additions & 0 deletions tinync/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Tinync
Tinync is a simplified implementation of [nc](https://en.wikipedia.org/wiki/Netcat), which aims to demonstrate the usage of [coroutine](https://en.wikipedia.org/wiki/Coroutine) macros and their effects.

## Internals
### Declaring a Coroutine Function
At the very beginning, a coroutine function should be declared first, coroutine function is part of program that will be executed simultaneously with other coroutines. All coroutines should be specified with the macro `cr_proto`, following shows the example:
```cpp
static void cr_proto(coroutine_name, parameter_declarations)
{
/* coroutine body */
}
```

### Controlling Macros
With in the coroutine, there are several macros for controlling the behavior of coroutine, following are list of them:
* `cr_begin` aims to initiate the context of coroutine when the coroutine is first invoked, or resume previous execution for the rest of invocations. This macro should be placed before other cotrolling macros except `cr_local`.
* `cr_end` ends up a coroutine that will mark the status of coroutine as finished, which could be detected outside to ensure whether a coroutine finisihed its job or not.
* `cr_wait` is used for waiting a condition happened. Once the condition it is waiting for has not happened yet, it will pause the current coroutine and switch to another.
* `cr_exit` not only yields a coroutine but also updates its state with given state.
* `cr_sys` is a wrapper of `cr_wait` which performs waiting on system calls and other functions that return -1 and set `errno`.
* `cr_local` is a marker for programmers to recognize a variable related to coroutine easily.

In `tinync.c`, we can see the combinations of these macros:
```cpp
static void cr_proto(stdin_loop, byte_queue_t *out)
{
/* b and r are variables used in coroutine whose
* value will be preserved across pauses.
*/
cr_local uint8_t b;
cr_local int r;

/* Initiates the context of this coroutine. */
cr_begin();
for (;;) {
/* Wait for read system call to become successful. */
cr_sys(r = read(STDIN_FILENO, &b, 1));
if (r == 0) {
/* Wait until queue out is flushed. */
cr_wait(cr_queue_empty(out));

/* Exit the coroutine with status update as finished. */
cr_exit(1);
}
/* Wait until there is place in queue out. */
cr_wait(!cr_queue_full(out));
cr_queue_push(out, b);
}
/* End up this coroutine, status will be updated as finished. */
cr_end();
}
```

### Coroutine Context
Context is an important part for coroutine, which preserves the execution point of a coroutine that could be resumed later. To define a context for a coroutine, use `cr_context` macro and initiates it with macro `cr_context_init`. It is important to **assign an identical name to context and its corresponding function**. With example presented at the beginning, its corresponding context should be specified as follows:
```cpp
cr_context(coroutine_name) = cr_context_init();
```

### Launching and Monitoring
Now, all required preparations are done, programmers may launch a coroutine via `cr_run` macro and monitor it with `cr_status` macro. To execute several coroutines simultaneously, place `cr_run`s that launch coroutines in a loop and keep tracking their status until all of them are finished. Following shows a simple example:
```cpp
while (cr_status(coroutine_1) != CR_FINISHED &&
cr_status(coroutine_2) != CR_FINISHED &&
cr_status(coroutine_3) != CR_FINISHED) {
cr_run(coroutine_1);
cr_run(coroutine_2);
cr_run(coroutine_3);
}
```

## Run the Sample Program
Tinync is a sample program that handles several coroutines to maintain communication with remote while accept user input simultaneously. To compile it, use `make`:
```shell
$ make
```
This sample requires two terminals, let's say `T1` and `T2`. Before launching `tinync`, start `nc` in `T1` first:
```shell
$ nc -l 127.0.0.1 9000
```
Then launch `tinync` in `T2`:
```shell
$ tinync 127.0.0.1 9000
```
Now, any words typed in `T1` will be recived and presented in `T2`, and vice versa.
117 changes: 64 additions & 53 deletions tinync/tinync.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,50 @@ enum {
struct cr {
void *label;
int status;
void *local; /* private local storage */
};

#define cr_init() \
#define cr_context_name(name) __cr_context_##name
#define cr_context(name) struct cr cr_context_name(name)
#define cr_context_init() \
{ \
.label = NULL, .status = CR_BLOCKED \
}
#define cr_begin(o) \
do { \
if ((o)->status == CR_FINISHED) \
return; \
if ((o)->label) \
goto *(o)->label; \

#define cr_func_name(name) __cr_func_##name
#define cr_proto(name, ...) cr_func_name(name)(struct cr * ctx, ##__VA_ARGS__)

#define cr_run(name, ...) \
cr_func_name(name)(&cr_context_name(name), ##__VA_ARGS__)

#define cr_local static

#define cr_begin() \
do { \
if ((ctx)->status == CR_FINISHED) \
return; \
if ((ctx)->label) \
goto *(ctx)->label; \
} while (0)
#define cr_label(o, stat) \
do { \
(o)->status = (stat); \
__cr_line(label) : (o)->label = &&__cr_line(label); \
} while (0)
#define cr_end(o) cr_label(o, CR_FINISHED)
#define cr_end() cr_label(ctx, CR_FINISHED)

#define cr_status(o) (o)->status
#define cr_status(name) cr_context_name(name).status

#define cr_wait(o, cond) \
do { \
cr_label(o, CR_BLOCKED); \
if (!(cond)) \
return; \
#define cr_wait(cond) \
do { \
cr_label(ctx, CR_BLOCKED); \
if (!(cond)) \
return; \
} while (0)

#define cr_exit(o, stat) \
do { \
cr_label(o, stat); \
return; \
#define cr_exit(stat) \
do { \
cr_label(ctx, stat); \
return; \
} while (0)

#define cr_queue(T, size) \
Expand All @@ -70,9 +80,9 @@ struct cr {
(cr_queue_empty(q) ? NULL : &(q)->buf[(q)->r++ % cr_queue_len(q)])

/* Wrap system calls and other functions that return -1 and set errno */
#define cr_sys(o, call) \
cr_wait(o, (errno = 0) || !(((call) == -1) && \
(errno == EAGAIN || errno == EWOULDBLOCK || \
#define cr_sys(call) \
cr_wait((errno = 0) || \
!(((call) == -1) && (errno == EAGAIN || errno == EWOULDBLOCK || \
errno == EINPROGRESS || errno == EINTR)))

#include <arpa/inet.h>
Expand All @@ -87,47 +97,47 @@ struct cr {

typedef cr_queue(uint8_t, 4096) byte_queue_t;

static void stdin_loop(struct cr *o, byte_queue_t *out)
static void cr_proto(stdin_loop, byte_queue_t *out)
{
static uint8_t b;
static int r;
cr_begin(o);
cr_local uint8_t b;
cr_local int r;
cr_begin();
for (;;) {
cr_sys(o, r = read(STDIN_FILENO, &b, 1));
cr_sys(r = read(STDIN_FILENO, &b, 1));
if (r == 0) {
cr_wait(o, cr_queue_empty(out));
cr_exit(o, 1);
cr_wait(cr_queue_empty(out));
cr_exit(1);
}
cr_wait(o, !cr_queue_full(out));
cr_wait(!cr_queue_full(out));
cr_queue_push(out, b);
}
cr_end(o);
cr_end();
}

static void socket_write_loop(struct cr *o, int fd, byte_queue_t *in)
static void cr_proto(socket_write_loop, byte_queue_t *in, int fd)
{
static uint8_t *b;
cr_begin(o);
cr_local uint8_t *b;
cr_begin();
for (;;) {
cr_wait(o, !cr_queue_empty(in));
cr_wait(!cr_queue_empty(in));
b = cr_queue_pop(in);
cr_sys(o, send(fd, b, 1, 0));
cr_sys(send(fd, b, 1, 0));
}
cr_end(o);
cr_end();
}

static void socket_read_loop(struct cr *o, int fd)
static void cr_proto(socket_read_loop, int fd)
{
static uint8_t b;
static int r;
cr_begin(o);
cr_local uint8_t b;
cr_local int r;
cr_begin();
for (;;) {
cr_sys(o, r = recv(fd, &b, 1, 0));
cr_sys(r = recv(fd, &b, 1, 0));
if (r == 0)
cr_exit(o, 1);
cr_sys(o, write(STDOUT_FILENO, &b, 1));
cr_exit(1);
cr_sys(write(STDOUT_FILENO, &b, 1));
}
cr_end(o);
cr_end();
}

static int nonblock(int fd)
Expand Down Expand Up @@ -176,23 +186,24 @@ int main(int argc, char *argv[])
};
connect(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));

struct cr cr_stdin = cr_init();
struct cr cr_socket_read = cr_init();
struct cr cr_socket_write = cr_init();
byte_queue_t queue = cr_queue_init();

while (cr_status(&cr_stdin) == CR_BLOCKED &&
cr_status(&cr_socket_read) == CR_BLOCKED) {
cr_context(stdin_loop) = cr_context_init();
cr_context(socket_read_loop) = cr_context_init();
cr_context(socket_write_loop) = cr_context_init();

while (cr_status(stdin_loop) == CR_BLOCKED &&
cr_status(socket_read_loop) == CR_BLOCKED) {
if (cr_queue_empty(&queue)) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
FD_SET(fd, &fds);
select(fd + 1, &fds, NULL, NULL, NULL);
}
socket_read_loop(&cr_socket_read, fd);
socket_write_loop(&cr_socket_write, fd, &queue);
stdin_loop(&cr_stdin, &queue);
cr_run(socket_read_loop, fd);
cr_run(socket_write_loop, &queue, fd);
cr_run(stdin_loop, &queue);
}

close(fd);
Expand Down