3

The code

#include <csetjmp>

template <typename Callable>
void create_checkpoint(std::jmp_buf buf, Callable&& callable)
{
    if (setjmp(buf) != 0)
    {
        callable();
    }
}

#include <iostream>

struct announcer {
    int id;
    announcer(int id):
        id{id}
    {
        std::cout << "created announcer with id " << id << '\n';
    }
    ~announcer() {
        std::cout << "destructing announcer with id " << id << '\n'; 
    }
};

void oopsie(std::jmp_buf buf, bool shouldJump)
{
    if (shouldJump)
    {
        // std::cout << "performing jump...\n";
        std::longjmp(buf, 1);
    }
}

void test1() 
{
    std::jmp_buf buf;
    announcer a1{1};
    create_checkpoint(buf, []() {throw std::exception();});
    oopsie(buf, true);
}

void test2()
{
    std::jmp_buf buf;
    announcer a1{1};
    create_checkpoint(buf, []() {throw std::exception();});
    oopsie(buf, false);


    announcer a2{2};
    create_checkpoint(buf, []() {throw std::exception();});
    oopsie(buf, true);
}

int main()
{
    try 
    {
        test1();
    }
    catch (...)
    {}

    try 
    {
        test2();
    }
    catch (...)
    {}
}

Context

I have to call some C library that reports errors via longjmp. To provide strong exception guarantee, I want to create a function that is very similar to std::lock_guard, e.g. I just write create_checkpoint(buf, handler) and keep calling the C library functions until I allocate more resources (destructors for objects created below setjmp line are not called, if I understand correctly).

Question

Why undefined behavior is invoked in this case and how can I fix it?

How did I find out this is undefined behavior?

Printing message to std::cout before std::longjmp vs not printing produces very different results even though that line has little to do with control flow.

What do I understand by now?

I understand that std::longjmp essentially restores the registers and jumps at the instruction pointer saved by setjmp macro. Also, functions are not optimized away, and at least during compilation, there is an instruction to call longjmp.

Converting the create_checkpoint to macro seems to solve the issue. Yet I wonder is there a better way to do this?

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
Incomputable
  • 2,188
  • 1
  • 20
  • 40
  • 1
    Any use of `setjmp`/`longjmp` in modern C++ should be *HEAVILY* discouraged. In fact, I'd consider any use a signal that the code in question should *never* be merged into the main code base. – Jesper Juhl Jan 11 '20 at 15:10
  • 2
    `longjmp` bypasses C++ stack unwinding, so destructors are not called. You fix it by either not using `longjmp`, or carefully catch each one as close to the part of the code that invokes it and convert it to an exception or handle it appropriately. Also note that `setjmp`/`longjmp` do not restore non-volatile variables at the `setjmp` location, so they are not trustworthy to be properly accounted for in the handler. – Eljay Jan 11 '20 at 15:10
  • 2
    I should mention, I've maintained a very large program that used `setjmp`/`longjmp` extensively. I strongly discourage using them in a C++ program, with every fiber of my being. They are the nuclear equivalent of `goto`. – Eljay Jan 11 '20 at 15:12
  • @Eljay, unfortunately in one of the cases it seems like it is unavoidable (libjpeg). We do not know if the whole call tree is correctly covered, so I thought having a guard function would be nice. Macro seems to solve the problem, but I am not sure if it is the right way forward. – Incomputable Jan 11 '20 at 15:14
  • Please vote for deletion if you think the question is useless. – Incomputable Jan 11 '20 at 15:19
  • 1
    I think this a good question. It's something C++ inherited from C, but can cause lots of grief if used in C++. – Eljay Jan 11 '20 at 15:23

2 Answers2

5

From https://en.cppreference.com/w/cpp/utility/program/longjmp

If the function that called setjmp has exited, the behavior is undefined (in other words, only long jumps up the call stack are allowed)

As you aren't following this rule your program has undefined behaviour

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
  • I see. So only macro can be created, and not a function? Would it be possible to somehow inline the function? Also, should I delete the question? It seems rather useless now. – Incomputable Jan 11 '20 at 15:07
  • 1
    Yep, a macro could probably do the job. No need to delete the question, someone else will likely have a similar question in the future – Alan Birtles Jan 11 '20 at 15:29
2

Code that populates a jmp_buff has to know what should be on left on the stack after it is passed to longjmp. If setjmp were processed as a compiler intrinsic that was only usable within a function that return int, the compiler could arrange things so that longjmp would cause the function that called setjmp to "return twice", rather than treating the setjmp itself as doing so. On many implementations, however, calls to setjmp are processed just like any other calls to functions about which the compiler's knowledge is limited to the prototype. On such implementations, there would be no way for setjmp to arrange to have the longjmp return to the calling function's caller without information about that function's stack frame. While the compiler processing the setjmp call would have the needed information, it would have no reason to make it available to setjmp, and setjmp would have no way of getting the information without such compiler support.

BTW, what's more vexing about setjmp is that while it's possible for it to return a value, setjmp calls must appear within a very narrow set of contexts, none of which can conveniently capture the value returned. One could in theory say:

int setJmpValue;
switch(setjmp(...))
{
  case 0: setJmpValue=0; break;
  case 1: setJmpValue=1; break;
  ...
  case INT_MAX-1: setJmpValue = INT_MAX-1; break;
  case INT_MAX  : setJmpValue = INT_MAX  ; break;
}

but that would be rather annoying and could not be exported to a function.

I don't think there should be any difficulty with allowing i = setJmp(...); where i is an int of either static or automatic duration, and that would in turn make possible any arbitrary use of the returned value, but no such construct is provided for in the Standard, and it is no longer fashionable for compilers to process useful constructs predictably except when the Standard compels them to do so.

supercat
  • 77,689
  • 9
  • 166
  • 211