2

I had a question about using setjmp and longjump to create function stacks that can run independently of one another. With reference to this question

Here the function stack for B() seems to be on top of the one for A so when A goes out of scope and I try longjumping to B() the code segfaults. The modified code looks like so

#include <stdio.h>
#include <setjmp.h>
#include <iostream>
using namespace std;

jmp_buf bufferA, bufferB;

void routineB(); // forward declaration 

void routineA()
{
    int r ;

    printf("(A1)\n");

    r = setjmp(bufferA);
    if (r == 0) routineB();

    printf("(A2) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20001);

    printf("(A3) r=%d\n",r);

    r = setjmp(bufferA);
    if (r == 0) longjmp(bufferB, 20002);

    printf("(A4) r=%d\n",r);
}

void routineB()
{
    int r;

    printf("(B1)\n");

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10001);

    printf("(B2) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10002);

    printf("(B3) r=%d\n", r);

    r = setjmp(bufferB);
    if (r == 0) longjmp(bufferA, 10003);

    cout << "WHAT" << endl;
}


int main(int argc, char **argv) 
{
    routineA();
    longjmp(bufferB, 123123);
    return 0;
}

I was thinking, we can have one main function each coroutine (in this case B and A) jumps back to which then in turn jumps to one of the coroutines given that they are alive. Will that work or will that also segfault because some of the stacks for coroutines that are possibly dead are on top of ones that want to run?

If it does seem like it should segfault, then how can one implement such independent couroutines (which can run when others have died and don't depend on each other) in C++?

NOTE: I do not want to use swapcontext, makecontext, setcontext, and getcontext since they are deprecated. The purpose of this post is to help me find alternatives to those functions

Thanks in advance!

Community
  • 1
  • 1
Curious
  • 20,870
  • 8
  • 61
  • 146
  • 2
    you can't `longjmp()` into a function deeper in the call stack than the currently-executing function. – The Paramagnetic Croissant Oct 29 '15 at 07:20
  • At least for c++ (and I believe C too) the referenced answer is just wrong. You are invoking undefined behavior. – MikeMB Oct 29 '15 at 07:55
  • It is possible to implement threading in C using setjmp/longjmp, I remember doing this in a University class. If you really need it I will dig into my brain to find out how. My first thought is that you need to allocate stack space and tweak the jmp_buf, and also to make it nice you need an alarm or something (unless you just want to yield to change running thread) that will trigger a longjmp to the next running thread. – mattiash Oct 29 '15 at 08:22
  • `how can one implement such independent couroutines (which can run when others have died and don't depend on each other)` using setjmp/longjmp you can't. What you want is _preemptive_ multitasking (or multithreading), which is only possible using some kind of privileged "supervisor". Though you can catch "deep errors", i.e. sort of "C exception handling". – Matt Oct 29 '15 at 15:09
  • @user441802, I'm not sure what you mean, It is certainly possible to implement threading with this approach. Coroutines imply something more than just threading, I assume. – mattiash Oct 29 '15 at 15:27

2 Answers2

0

It should work, with some precautions. It used to work in the nineties.

It works as long as the thread that is older in the stack does not return, then there should be no issue. For a generator or a thread, it means that it terminates with some final yield, but no return.

I remember asking IBM to fix their AIX implementation of setjmp/longjmp so that it would stop checking (and forbidding) that usefull behaviour that other implementation allowed. That was the most portable way I found back then to implement cooperative threads.

You can do even better with a state machine based stack allocator in order to allocate/free stacks of different sizes to different thread.

That's very tricky however as far I a remember. I remember Matz (from Ruby) asking me years later about the code for that but alas I had no more access to it at the time.

Anyways, this does not matter much nowadays because pthread does the job.

JeanHuguesRobert
  • 696
  • 5
  • 10
-1

Quick and dirty hack on Windows. Sorry for the messy naming and messy code.

    // mythreads.cpp : Defines the entry point for the console application.
//

#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include "List.h"

#include <Windows.h>

struct thread_t 
{
  util::Node node;
  jmp_buf buf;
};

util::List thread_list;
thread_t* runningThread = NULL;

void schedule(void);

void thread_yield()
{
  if (setjmp(runningThread->buf) == 0)
  {
    thread_list.push_back(&runningThread->node);
    schedule();
  }
  else
  {
    /* EMPTY */
  }
}

void myThread1(void *args)
{
  printf("myThread1\n");
}

void startThread(void(*func)(void*), void *args)
{
  thread_t* newThread = (thread_t*)malloc(sizeof(thread_t));

  // Create new stack here! This part is Windows specific. Find the current stack
  // and copy the contents. One might do more stuff here, e.g. change the return address 
  // of this function to be a thread_exit function.
  NT_TIB* tib = (NT_TIB*)__readfsdword(0x18);
  uint8_t* stackBottom = (uint8_t*)tib->StackLimit;
  uint8_t* stackTop = (uint8_t*)tib->StackBase;

  size_t stack_size = stackTop - stackBottom;
  uint8_t *new_stack = (uint8_t*)malloc(stackTop - stackBottom);
  memcpy(new_stack, stackBottom, stack_size);

  _JUMP_BUFFER *jp_buf = (_JUMP_BUFFER*)newThread->buf;

  if (setjmp(newThread->buf) == 0)
  {
    // Modify necessary registers to point to new stack. I may have 
    // forgotten a bunch of registers here, you must do your own homework on
    // which registers to copy.
    size_t sp_offset = jp_buf->Esp - (unsigned long)stackBottom;
    jp_buf->Esp = (size_t)new_stack + sp_offset;
    size_t si_offset = jp_buf->Esi - (unsigned long)stackBottom;
    jp_buf->Esi = (size_t)new_stack + si_offset;
    size_t bp_offset = jp_buf->Ebp - (unsigned long)stackBottom;
    jp_buf->Ebp = (size_t)new_stack + bp_offset;
    thread_list.push_back(&newThread->node);
  }
  else
  {
    /* This is where the new thread will start to execute */
    func(args);
  }
}

void schedule(void)
{
  if (runningThread != NULL)
  {

  }

  if (thread_list.size() > 0)
  {
    thread_t* t = (thread_t*)thread_list.pop_front();
    runningThread = t;
    longjmp(t->buf, 1);
  }
}

void initThreading()
{
  thread_list.init();

  thread_t* mainThread = (thread_t*)malloc(sizeof(thread_t));

  if (setjmp(mainThread->buf) == 0)
  {
    thread_list.push_back(&mainThread->node);
    schedule();
  }
  else
  {
    /* This is where the main thread will start to execute again */
    printf("Main thread running!\n");
  }
}

int main()
{
  initThreading();

  startThread(myThread1, NULL);

  thread_yield();

  printf("Main thread exiting!\n");
}

Also, Microsoft and setjmp/longjmp might not be a good match. If I have had a Unix box nearby I would have done it on that one instead.

And I would probably have to study a bit more about the stack copy to make sure it works correctly.

EDIT : To check which registers to modify after stack change one might consult the compiler manual.

The code starts in main() by setting up the threading framework. It does this by treating your main thread (the only thread at this point) as just a thread among others. So, store the context for the main thread and put it in the thread_list. Then we schedule() to let one thread run. This is done in initThreading(). Since the only thread running is the main thread, we will continue in the main() function.

The next thing the main function does, is startThread with a function pointer as parameter (and the arg NULL to be sent to the func). What the startThread() function does is that it allocates memory for a stack for the new thread (every thread needs its own stack). After allocation, we save the context of the running thread into the new thread context buffer (jmp_buf) and change the stack pointers (and allo other registers that should be changed) so that it points to the newly created stack. Then we add this new thread to the list of waiting threads. And then we return from the function. We are still in the main thread.

In the main thread we do thread_yield() which says "OK, I don't want to run anymore, let someone else run!". The thread_yield function stores the current context and puts the main thread at the back of the thread_list. Then we schedule.

Schedule() takes the next thread from thread_list and does a longjmp to the context saved in the thread buf (jmp_buf). In this case the thread will continue to run in the else clause of the setjmp where we stored the context.

  else
    {
        /* This is where the new thread will start to execute */
        func(args);
      }

It will run until we do a thread_yield and then we will do the same thing, but instead take the main thread and do a longjmp on its saved buffer and so on and so on...

If one wants to be fancy and want time slices, one can implement some alarm to save the current thread context and then call schedule().

Any clearer?

mattiash
  • 172
  • 7
  • Sorry I'm having a very hard time understanding this. Could you try and explain? Or could you link me to your university class that implemented this and had an explanation? – Curious Oct 29 '15 at 13:44
  • Ah, I took that class 20 years ago...I will edit my answer to try and explain. – mattiash Oct 29 '15 at 14:47
  • Thanks for the explanation! The only part I was confused about in your code was the part where you modify the stack in the jmp_buf. What's happening there? – Curious Oct 29 '15 at 15:09
  • Well, you need every thread to execute with its own stack. So you need a new stack for your new thread. After allocating memory for that, you need to change a couple of things in the context so that the new thread starts using the new stack. What platform are you on? Windows, Linux or something else? – mattiash Oct 29 '15 at 15:15
  • Here's a good post on stack base pointers etc. http://stackoverflow.com/questions/1395591/what-is-exactly-the-base-pointer-and-stack-pointer-to-what-do-they-point – mattiash Oct 29 '15 at 15:17
  • So I had this one question regarding setjmp and longjmp. When a longjmp is called and the stack of function B is on top of the stack of function A, does the stack for function B get destroyed? So when the longjmp from B to A occurs, can A start off another "thread" in the form of a setjmp and function call to another function? Sorry if that was confusing. Please let me know if I can clarify that for you – Curious Oct 29 '15 at 18:02
  • Maybe you have to clarify this more. As it is in your original code you are jumping back to a stack position which is no longer valid. – mattiash Oct 30 '15 at 07:48
  • 1
    I haven't analyzed your code, but do you know if this is guaranteed to work, or if it just happens to work - there are a lot of cases in c++ where calling longjmp invokes undefined behavior. Especially interesting is that quote from [cppref](http://en.cppreference.com/w/cpp/utility/program/longjmp): *"If replacing of std::longjmp with throw and setjmp with catch would execute a non-trivial destructor for any automatic object, the behavior of such std::longjmp is undefined. *" I havn't checked back with the standard though. – MikeMB Nov 02 '15 at 08:19
  • @MikeMB, I don't guarantee anything. This is a quick and dirty solution that happens to work, but it outlines the necessary steps to create such a solution. With this kind of solution I would have stuck to C, but I had a C++ implementation of a non-STL list and just wanted to use it. In C everything is simpler naturally. I'm pretty sure your quote is correct, I think I have seen it before. But like I said, a 10 min solution to outline the necessary steps. – mattiash Nov 02 '15 at 08:40
  • Does not compile at all. – LXSoft Nov 29 '22 at 16:24