Switch to new call stack in C?
Asked Answered
F

3

5

I am writing a toy programming language and would like to implement cooperative multi-tasking. The compiler and runtime are written in C.

Instead of using threads, I would like to be able to switch the C call stack. I.e. I would like to have multiple call stacks "alive" and switch between them by calling a function. This should be conceptually similar to a longjmp but preserve the old/new call stack. The goal is essentially to implement something like "async" runtimes in other languages.

I would like to do this without a library in the "simplest" way possible. My idea is to essentially just allocate a new C call stack and switch to it. Is there any way to do this in C in a platform agnostic way (i.e. without assembly)?

The closest thing I found was a small block of assembly in this answer (linked from this question)

Edit: by "platform agnostic" I meant hardware agnostic using linux, although general Unix/BSD support would be nice.

Feininger answered 15/6, 2022 at 2:55 Comment(5)
it'll be easier in C++ where you can use the built-in coroutine feature. In C you might be able to use setjmp/longjmpLaaspere
Unless I'm mistaken, setjmp / longjmp save the current stack state. Basically a longjmp will restore the current stack to the state it existed at setjmp. What I need is a new stack.Feininger
setjmp will save the current stack, so next you can jump to another place and have another stack, then use longjmp to restore the current stack. There are also a few other ways to implement coroutines in C: Coroutines in C, Coroutines in C or C++?, How can I create a parallel stack and run a coroutine on it?Laaspere
I don't believe that's how setjmp/longjmp works. It rather saves the current stack location. It doesn't make a copy of the entire stack, that would be incredibly expensive. setjmp/longjmp work if you are (1) about to call a bunch of nested/recursive functions (2) deep in the nesting/recursion you want to jmp back to 1. It basically "throws away" your current stack, restoring it much deeper down the stack. BTW I use longjmp for panic handling, it's great! The case you gave of "coroutines" is jumping back and forth between two functions. They have the same stack depth.Feininger
@titiral who said that? coroutines are just "threads in a single thread" and each will have their own stack space. They're also called fibers which Unix ucontext, Windows Fibers, GNU Portable Threads or boost.fiber are all about and they're used to implement coroutines in higher-level languages like golang's goroutine and Lua/Python/Kotlin/C++ coroutineLaaspere
S
4

It is unclear what answer will satisfy you.

The C language by itself does not provide any such facility, so any solution will rely on something else providing the functionality.

Note that since C.2011, C introduced optional thread support in <threads.h>, but there are no thread attribute manipulations possible to allow one to distinguish between ordinary and lightweight thread execution.

Maybe a state-machine, but needs help from the OS.

If you are asking how one achieves this using only the primitives provided by the C language, then the answer choice is pretty much limited to implementing your own state machine mechanism. But this would require that you use mechanisms provided by your operating system to enable non-blocking interactions when accessing operations that would normally block until completion. This is to give you the opportunity to park the current execution context and try an alternate one instead.

For example, in UNIX, one could create a state machine data structure to indicate the progress of sending a file over a socket.

struct send_a_file_ctx {
    int socket;
    int file;
    size_t file_size;     /* total number of bytes to send */
    size_t bytes_sent;    /* bytes sent so far over the socket */
    size_t buffer_size;   /* number of file bytes in the buffer */
    size_t buffer_offset; /* number of file bytes from buffer sent */
    char buffer[];        /* buffered chunk of the file */
};

There is no execution stack per se, but there is a structure that is tracking what is going on with the delivery of the bytes in the buffer onto the socket.

In UNIX, the send() call could have the MSG_DONTWAIT flag passed in to indicate that the operation should not block for completion, but instead return a value to indicate that the operation would have blocked. This would be indicated with an error result and errno set to EAGAIN or EWOULDBLOCK. This context structure should then be saved in some queue to be picked up again in the future to retry the operation. The same queue could have some other context structure in it upon which the send() could be attempted again to see if progress can be made.

Instead of continuously looping on a retry, UNIX provides mechanisms to allow you to wait for an indication that one or more of the sockets you are trying to send files over will no longer block (see poll, select, epoll, kevent, and perhaps other variations depending on your OS).

What about ucontext.h?

The makecontext() and swapcontext() functions is the mechanism classic UNIX systems had provided for cooperative concurrency. It lets a process switch its current execution stack with an alternate execution stack. Because it lets you switch execution stacks, it can avoid the need for defining a state machine structure. The state is intrinsic to the execution stack.

However, it does not avoid need to enable non-blocking interactions with operations that would normally block until completion. Upon receiving an indication the operation would block, you would switch to an alternate stack to retry their previously blocked operation.

And again, to avoid continuous retries, you would still need to implement some kind of scheduling queue of stacks to resume, and use a demultiplexing waiting primitive, like poll, to know when which stack can be resumed and not get blocked.

While Linux and BSD systems still support ucontext.h, it is no longer part of the POSIX standard. It was part of the original POSIX.1-2001, but was marked for obsolescence in POSIX.1-2004. In POSIX.1-2008, it was removed.

Are there any good alternatives?

It depends on what you would consider "good".

State machines?

If you want something that takes care of demultiplexing for you, and you just want to write code to run your state machine on resumption, you can consider using something similar to libevent or one of its forks/clones. It provides a common interface for registering your context with the demultiplexer, and works on a variety of operating systems.

However, you still need to manage non-blocking interactions yourself.

Like threads?

If you want a solution that allows you easy programming like with threads, and avoid managing your own non-blocking context scheduling, you can try GNU's pth, Libwire, Windows Fibers, or even pthreads.

  • GNU pth can provide a thread like programming paradigm. If you follow the provided APIs, it will appear like all your threads are performing block to completion operations, when pth is actually intercepting the calls and performing the non-blocking versions under the covers and context switching on your behalf.

  • If LGPL is too restrictive, you may consider using Libwire, which is released under the MIT license. This is a user-space threading library, and is meant to bring a GoLang like coroutine programming interface to C.

  • Windows Fibers is a facility that provides lightweight threading, while maintaining a block to completion programming style. Fibers has a noteworthy feature where a fiber can be converted into a thread, and back to a fiber again.

  • POSIX pthreads defines a contentionscope parameter to pthread_attr_setscope. One of the values for this parameter is PTHREAD_SCOPE_PROCESS. It seems like this would be the way to achieve lightweight scheduling using pthreads. However, the behavior of this scope is implementation defined.

References

Sur answered 15/6, 2022 at 7:7 Comment(1)
This is an excellent overview! ucontext seems like possibly the right solution. I'm also checking out async.h as suggested by another answer. I can't use a GNU library as I wish for my code to remain as close to public domain as possible.Feininger
R
5

Is there any way to do this in C in a platform agnostic way (i.e. without assembly)?

Many UNIX platforms provide makecontext / swapcontext which allows for a pretty simple use (see example in the linked man page).

Racehorse answered 15/6, 2022 at 3:35 Comment(5)
It's a bit of a stretch to consider this platform agnostic. It's not assembly, so that's something, But it's not part of the C standard. It's not even part of the POSIX standard. Since the OP raised portability as a concern, the answer should identify these portability issues.Biotic
@Biotic I added a note on my question that Iby "platform agnostic" I meant hardware agnostic using Linux. Thanks for making me clarify!Feininger
It does seem to be supported in FreeBSD and NetBSD, along with Linux. Good enough for me!Feininger
@Feininger that's obvious because makecontext/swapcontext are in the POSIX standardLaaspere
No, they were removed in 2008. If you click the link for the "newer version" you won't find makecontext. So not obvious, but still good enough for me!Feininger
S
4

It is unclear what answer will satisfy you.

The C language by itself does not provide any such facility, so any solution will rely on something else providing the functionality.

Note that since C.2011, C introduced optional thread support in <threads.h>, but there are no thread attribute manipulations possible to allow one to distinguish between ordinary and lightweight thread execution.

Maybe a state-machine, but needs help from the OS.

If you are asking how one achieves this using only the primitives provided by the C language, then the answer choice is pretty much limited to implementing your own state machine mechanism. But this would require that you use mechanisms provided by your operating system to enable non-blocking interactions when accessing operations that would normally block until completion. This is to give you the opportunity to park the current execution context and try an alternate one instead.

For example, in UNIX, one could create a state machine data structure to indicate the progress of sending a file over a socket.

struct send_a_file_ctx {
    int socket;
    int file;
    size_t file_size;     /* total number of bytes to send */
    size_t bytes_sent;    /* bytes sent so far over the socket */
    size_t buffer_size;   /* number of file bytes in the buffer */
    size_t buffer_offset; /* number of file bytes from buffer sent */
    char buffer[];        /* buffered chunk of the file */
};

There is no execution stack per se, but there is a structure that is tracking what is going on with the delivery of the bytes in the buffer onto the socket.

In UNIX, the send() call could have the MSG_DONTWAIT flag passed in to indicate that the operation should not block for completion, but instead return a value to indicate that the operation would have blocked. This would be indicated with an error result and errno set to EAGAIN or EWOULDBLOCK. This context structure should then be saved in some queue to be picked up again in the future to retry the operation. The same queue could have some other context structure in it upon which the send() could be attempted again to see if progress can be made.

Instead of continuously looping on a retry, UNIX provides mechanisms to allow you to wait for an indication that one or more of the sockets you are trying to send files over will no longer block (see poll, select, epoll, kevent, and perhaps other variations depending on your OS).

What about ucontext.h?

The makecontext() and swapcontext() functions is the mechanism classic UNIX systems had provided for cooperative concurrency. It lets a process switch its current execution stack with an alternate execution stack. Because it lets you switch execution stacks, it can avoid the need for defining a state machine structure. The state is intrinsic to the execution stack.

However, it does not avoid need to enable non-blocking interactions with operations that would normally block until completion. Upon receiving an indication the operation would block, you would switch to an alternate stack to retry their previously blocked operation.

And again, to avoid continuous retries, you would still need to implement some kind of scheduling queue of stacks to resume, and use a demultiplexing waiting primitive, like poll, to know when which stack can be resumed and not get blocked.

While Linux and BSD systems still support ucontext.h, it is no longer part of the POSIX standard. It was part of the original POSIX.1-2001, but was marked for obsolescence in POSIX.1-2004. In POSIX.1-2008, it was removed.

Are there any good alternatives?

It depends on what you would consider "good".

State machines?

If you want something that takes care of demultiplexing for you, and you just want to write code to run your state machine on resumption, you can consider using something similar to libevent or one of its forks/clones. It provides a common interface for registering your context with the demultiplexer, and works on a variety of operating systems.

However, you still need to manage non-blocking interactions yourself.

Like threads?

If you want a solution that allows you easy programming like with threads, and avoid managing your own non-blocking context scheduling, you can try GNU's pth, Libwire, Windows Fibers, or even pthreads.

  • GNU pth can provide a thread like programming paradigm. If you follow the provided APIs, it will appear like all your threads are performing block to completion operations, when pth is actually intercepting the calls and performing the non-blocking versions under the covers and context switching on your behalf.

  • If LGPL is too restrictive, you may consider using Libwire, which is released under the MIT license. This is a user-space threading library, and is meant to bring a GoLang like coroutine programming interface to C.

  • Windows Fibers is a facility that provides lightweight threading, while maintaining a block to completion programming style. Fibers has a noteworthy feature where a fiber can be converted into a thread, and back to a fiber again.

  • POSIX pthreads defines a contentionscope parameter to pthread_attr_setscope. One of the values for this parameter is PTHREAD_SCOPE_PROCESS. It seems like this would be the way to achieve lightweight scheduling using pthreads. However, the behavior of this scope is implementation defined.

References

Sur answered 15/6, 2022 at 7:7 Comment(1)
This is an excellent overview! ucontext seems like possibly the right solution. I'm also checking out async.h as suggested by another answer. I can't use a GNU library as I wish for my code to remain as close to public domain as possible.Feininger
F
1

I found a great small portable library for this here, in case anyone is interested.

Festatus answered 15/6, 2022 at 7:20 Comment(2)
I LOVE this. Thank you so much!Feininger
This would be an excellent choice for small call stacks. However, it requires essentially re-traversing the call stack every time you try to run an async stack. Granted, most calls will be reduced to a single switch statement... but this could still have a cost as the stack grows.Feininger

© 2022 - 2024 — McMap. All rights reserved.