The following code attempts to create a simple stackful coroutine. It allocates a stack frame in the heap space by setting the rsp register and then calling a function. Afterwards, it exits the coroutine via longjmp. However, using longjmp on the coroutine frame (the coro_test function) will cause a crash, regardless of whether the setjmp is located in the main stack or the coroutine stack.
So, my question is:
1.In theory, setjmp just saves all registers, and longjmp just restores the values of all registers saved by setjmp. Therefore, they can be used for stack frame switching in coroutines. Is this point incorrect? If it's incorrect, please point it out.
2.If using setjmp and longjmp is unsafe, how should I implement coroutine stack frame switching?
My compiler is GCC 14.2.0 x86_64-w64-mingw32.
#include <iostream>
#include <functional>
#include <cstdint>
#include <csetjmp>
#include <memory>
#include <cstdint>
struct coroutine_context
{
std::jmp_buf exit;
std::size_t stack_memory_size = 0;
std::unique_ptr<std::byte[]> stack_memory;
std::function<void(coroutine_context&)> coroutine_body;
int value_thoroughfare = 0;
};
#define SET_RSP(new_rsp) \
asm volatile ( \
"mov %0, %%rsp" \
: \
: "r" (new_rsp) \
: "memory" \
)
std::uintptr_t get_stack_base(const coroutine_context& context)
{
const static bool towards_address_LSB = []() -> bool
{
volatile int a;
volatile int b;
return &a > &b;
}();
if(towards_address_LSB)
return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get() + context.stack_memory_size) & (~0xF));
else
return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get()) & (~0xF)) + 0x10;
}
#if 0
std::jmp_buf coro_jmp_buf;
void coro_test(coroutine_context& context)
{
if(setjmp(coro_jmp_buf) == 0)
// Note: Even if the position where setjmp is called is within a coroutine, longjmp will crash after being called.
std::longjmp(coro_jmp_buf, 1);
context.value_thoroughfare = 114514;
std::longjmp(context.exit, 1);
}
#else
void coro_test(coroutine_context& context)
{
context.value_thoroughfare = 114514;
std::longjmp(context.exit, 1); // The program crashes here.
}
#endif
void create_coroutine_stack_frame(coroutine_context& context)
{
if(setjmp(context.exit))
return;
SET_RSP(get_stack_base(context));
coro_test(context);
}
int main()
{
coroutine_context context;
context.stack_memory_size = 1024 * 16;
context.stack_memory = std::make_unique<std::byte[]>(1024 * 16);
context.coroutine_body = coro_test;
create_coroutine_stack_frame(context);
std::cout << context.value_thoroughfare;
return 0;
}
The #if 0 part of the above code shows another crash case, with setjmp and longjmp both called inside the same coroutine so it's not switching stacks.
Answer to Q1:
You're right that setjmp saves registers and longjmp restores them, but they assume stack continuity. Calling longjmp from a fake, heap-allocated stack breaks this, causing a crash.
Answer to Q2:
Use a proper context switching mechanism (assembly, Boost.Context, Windows Fibers). Do not use setjmp/longjmp for coroutine stack switching — it’s unsafe and non-portable.