C/C++ implementations where longjmp unwinds?
Asked Answered
P

2

14

Are there major C/C++ implementations where the longjmp function "unwinds", i.e. where it interacts with destructors for automatic-storage objects, __attribute__((__cleanup__(...))), POSIX threads cancellation handlers, etc. rather than just restoring the register context saved by setjmp? I'm particularly interested in the existence (or non-existence) of POSIX implementations with this property, but C/C++ in general are also interesting.

For the bounty, I'm looking for a POSIX-conforming or at least POSIX-like system, as opposed to Windows which has already been mentioned.

Particiaparticipant answered 28/8, 2014 at 20:23 Comment(21)
I seriously doubt it.Naif
That would most likely break some of the use-cases for longjmp.Oakum
I seriously hope there isn't any as it would break about everything longjmp is used for.Rai
Yes, your favorite, MSVC++ implements it.Bernhard
Wasn't longjmp() 's purpose to do something like exception processing before exception did exist ?Mudstone
@HansPassant, are you sure ? Because msdn.microsoft.com/en-us/library/3ye15wsy.aspx says the opposite: it unwinds the stack but it doesn't call destructors !Mudstone
@Christophe. But msdn.microsoft.com/en-us/library/yz2ez4as.aspx says that it does call destructors if you compiled with /EH.Eats
I'd recommend you to throw an exception, an catch it in place you want to stop unwinding.Unset
@GingerPlusPlus: I'm not asking because I want to use such functionality (that would be utterly hideous) but because I need to know whether such implementations exist as part of a discussion of implementing other things that might be affected by it.Particiaparticipant
@Eats Oh yes ! I just did a test, and I wondered how come that it works ! Thanks for the tip !Mudstone
Not sure why Windows hasn't been added as an answer, since it seems to be one. I'd really like to find some POSIX[-like] examples though, so I've added a bounty and a note that I'm looking for more than just Windows for an answer to qualify for the bounty.Particiaparticipant
To be clear, you don't care if the behavior is legal, you just care if such an implementation exists? Because, C++11 says using setjmp/longjmp on code where unwinding would call non-trivial destructors causes undefined behavior.Rey
@jxh: Indeed. Per the standard language, it's undefined behavior. But individual implementations might define a behavior for it. Ideally it would be nice to see one where the behavior is actually "defined" by documentation, but I'd be interested in seeing one where it's not officially defined/supported but it ends up being what happens when you invoke the undefined behavior of longjmp'ing over dtors, etc.Particiaparticipant
Does Interix/SUA then not count as POSIX-like? It does have GCC, but its default compiler is the Visual Studio compiler. (Actually, that should be tested: I'm not sure how much of the implementation is in the library, and the libraries are likely sufficiently different that the mere fact that it works on Win32 provides no guarantees.)Helbonia
@hvd: Yes, Interix should count, but it's not clear to me that Interix would use the same setjmp/longjmp implementation as what's been discussed as "Windows", which is presumably the normal winapi subsystem. If you could confirm that Interix has this property, that would be a good answer.Particiaparticipant
Why do you ask? I guess you don't want longjmp to unwind, but why exactly?Stanwin
@BasileStarynkevitch: my guess is that it has something to do with the condition variable wait cancellation mentioned at openwall.com/lists/musl/2014/09/05/2Exceptive
@ninjalj: Actually not. It's related to austingroupbugs.net/view.php?id=863Particiaparticipant
Interix could only count, if you use gcc or at least not using /EHa, because this feature introduces non-POSIX-compliant behaviour to your program (SEH/signals problems). So, if Interix count, then you won't have another behaviour to setjmp/longjmp, because /EHa will make your program incompatible with POSIX? There are always two sides of a medal. The system, that supports compliance and the program, that uses compliance.Afterbody
@StefanWeiser: Do you have a citation on how SEH breaks signals?Particiaparticipant
Look at the recent comment on https://mcmap.net/q/839096/-c-c-implementations-where-longjmp-unwindsAfterbody
H
0

Interix (SUA) by default does not call destructors, but in x86 mode, does have an option for it.

Taking this test program, saved as test.cc:

#include <stdio.h>
#include <setjmp.h>

struct A {
  ~A() { puts("~A"); }
};

jmp_buf buf;

void f() {
  A a;
  longjmp(buf, 1);
}

int main() {
  if (setjmp (buf))
    return 0;

  f();
}

here's how Interix behaves. For brevity, I've omitted the required proper setup of PATH.

$ cc -mx86 test.cc && ./a.out
$ cc -mx86 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
~A
$ cc -mamd64 test.cc && ./a.out
$ cc -mamd64 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
$ 

The comments suggest that cc -X /EHa does not conform to POSIX, for example because /EHa would catch signals. That is not entirely true:

$ cat test.cc
#include <signal.h>
int main() {
  try {
    raise(SIGFPE);
  } catch (...) {
    // ignore
  }
}
$ cc -mx86 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
Floating point exception (core dumped)

If I change raise(SIGFPE) to a division by zero, I do indeed see that the exception handler catches it, but neither POSIX nor C++ requires any particular behaviour for that, so that doesn't affect conformance. It is also not the case that all asynchronous signals are caught: for this program:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void sigint(int signal) {
  puts("sigint");
  exit(0);
}
int main() {
  signal(SIGINT, sigint);
  try {
    for (;;) ;
  } catch (...) {
    // ignore
  }
}

"sigint" is printed after Ctrl-C, as expected. I don't see a reason for claiming that this implementation fails to meet POSIX requirements.

Helbonia answered 6/9, 2014 at 14:29 Comment(25)
Based on my reading of msdn.microsoft.com/en-us/library/1deeycx5.aspx, it sounds like with /EHs, the compiler just "optimizes out" the destructor path thinking that there's no way an exception can happen, and perhaps longjmp is still unconditionally doing unwinding. What happens if you add something like static volatile int x; if (x) throw(...); just before the longjmp?Particiaparticipant
@R.. One thing I had already tried was moving the longjmp to a separate function, and that had no effect: the destructor would be called only if /EHa is passed. I've now also tried your suggestion, and that too makes no difference.Helbonia
@R.. What's confusing is that the /EH* documentation seems to be saying the exact opposite of what it actually says. I think that what's happening, and I'm not entirely sure my understanding is correct, is that longjmp is implemented using SEH, and the /EHa option forces f to set up both a SEH handler and a C++ exception handler. With /EHs, only a C++ exception handler is set up. And on amd64, longjmp is not implemented using SEH.Helbonia
Interesting. Thanks for the clarifications.Particiaparticipant
IMHO SEH breaks ISO C++ standard compatibility. I don't think, that /EHa should be taken into account, because you normally implement crossplatform by using the least common denominator. It isn't even implemented platform independent inside of Interix itself. Further it conflicts with POSIX signals, because even those will be thrown and caught.Afterbody
@StefanWeiser In what way does it fail to conform to any requirement of C++? Anyway, the point of the question, as I understand it, isn't so much that you can or should rely on cleanup functions (e.g. destructors) getting called on this particular implementation, but more that you shouldn't rely on them not getting called.Helbonia
Your program isn't portable anymore, because it uses a compiler specific extension. This extension is implemented in a standard compliant way, but using it will break standard compliance for your code. However: SEH is only available on Microsoft products. It throws an exception on e.g. division-by-zero exceptions. POSIX-compliant programs would call a signal handler or terminate the program (POSIX-signals). So I don't even think of turning it on in case of developing crossplatform...Afterbody
@StefanWeiser All you're showing is that code that relies on platform-specific behaviour is platform-specific. Well of course. I never suggested otherwise.Helbonia
There are always two sides of a medal. The system must support POSIX-conformity and the program has to use it. But for the bounty, R.. is searching for a POSIX-compliant implementation (1st property), that calls destructors during unwind (2nd property). Sure, Interix supports both properties, but the 2nd one is only supported, if you compile the program in a way, where the 1st property isn't supported. So I think this is problematic. BTW even Microsoft warns their users about using SEH and /EHa for portable programs.Afterbody
@StefanWeiser It's a fair point that if cc -X /EHa is not a POSIX-conforming implementation, then it doesn't answer what the OP asked. I've yet to see evidence that it mishandles any valid program: POSIX doesn't define the interaction between signals and exception handlers, because POSIX doesn't define anything relating to C++ at all. Nonetheless, I'll run some more tests when I get the chance.Helbonia
Indeed. If @StefanWeiser is going to claim that enabling SEH breaks POSIX conformance of Interix, that claim needs to be supported.Particiaparticipant
I have no link, but it is easy to prove. If the compilation with /EHa won't break POSIX signals, then the following program would have an exit code != 0, because of an fatal error, as it does e.g. here on CentOS: ideone.com/HpOmzf. This is because POSIX defines, that the exit code is set to an value != 0 in case of an unhandled signal. Compiling it with /EHa let it return 0, because every exception is caught (the div0 exception too) as msdn.microsoft.com/en-us/library/1deeycx5.aspx claims.Afterbody
@StefanWeiser: That program invokes undefined behavior, so POSIX has nothing to say about what it does. If you change it to raise(SIGFPE), does the same thing happen?Particiaparticipant
@StefanWeiser Not only is the behaviour undefined of your program as R.. points out, it wouldn't even demonstrate your point if it were defined. POSIX allows catch(...) to have an effect on raised signals simply because it doesn't say anything whatsoever at all about C++. (I haven't tested anything yet.)Helbonia
@hvd, @R..: I guess, that we don't speak in the same terms of compatibility. A program is POSIX compatible IMO, if it will result in the same behaviour on every POSIX compatible system. You could exchange the SIGFPE with any other signal, that MS throws and therefore catches. BTW the behaviour is undefined as long as it is not raised by raise or kill. So I could update the program using raise... With the same result...Afterbody
Does it actually have the same result with raise? That's what I asked. The answer is not obvious unless you understand how Windows/Interix implements signals (which I don't). In any case, like hvd said, a C++ program can't really determine anything about POSIX conformance since POSIX does not specify the behavior of C++ programs.Particiaparticipant
Perhaps you could try a C program with pthread_cleanup_push instead of try/catch? Then it would all be in the domain that's governed by POSIX.Particiaparticipant
This whole discussion is ridiculous. Interix ships with a gcc to compile POSIX compatible. You cannot misuse a system, that has very different APIs (POSIX compatible and non compatible), by enforcing behaviour, that is clearly that of the non-compliant API, and then ask for POSIX compliance...Acescent
@R..: Yes it has the same result with raise. POSIX defines the behaviour of an OS interface. If this interface is compatible and the program is, the program should behave equal on all compliant systems. That is why people are defining portable interfaces. BTW: If I would compile a C program, how could I enforce the C++ feature SEH? I think SEH and signals are two models to handle fatal errors among others and there should be no reason to mix both (Microsoft does suggest this too in the docs on SEH).Afterbody
@Acescent No, it doesn't. It ships GCC to deal with lots of non-portable code out there that cannot compile with any other compiler than GCC. It uses CL in its own cc and c89 compilation scripts. As for non-compliance, please show proof already or stop claiming it's non-conforming. Merely re-asserting the same thing comment after comment after comment is not productive. Stefan Weiser has now claimed specific behaviour with raise, and that's testable, that's the way to convince others that they're wrong, and something I'll check when I can. (The machine isn't turned on often.)Helbonia
@StefanWeiser FWIW though, you cannot claim that a program is POSIX-compatible if it behaves the same way on all POSIX-compatible implementations, and then conclude based on the fact that one particular implementation makes it behave differently, that that implementation isn't POSIX-compatible. It would mean that either the program or the implementation isn't POSIX-compatible, but you cannot conclude which based on merely that. You've given a good example now to check against the other requirements of POSIX, and perhaps even of pure ISO C++.Helbonia
@hvd: Do you have any additional information I should look at as a basis for assigning a bounty? :-)Particiaparticipant
@R.. I may be able to run the tests based on Stefan Weiser's comments later today, but I have no additional information at this time.Helbonia
@R.. Added a few basic tests. If there's something you'd like to see tested too, please let me know.Helbonia
@hvd: I think you've provided good evidence that Interix has the properties I was asking about, so here's the +300. :-) I'll leave the answer unaccepted a bit longer in case any other interesting answers show up.Particiaparticipant
M
4

I'm trying to understand the logical goals that are trying to be accomplished here.

The setjmp(3) manual page states:

setjmp() saves the stack context/environment in env for later use by longjmp(3). The stack context will be invalidated if the function which called setjmp() returns.

This says that if you return from the stack context where the setjmp() call was made, you can no longer longjmp back to it. Otherwise, undefined behavior.

Ok, So it seems to me that at the point that a valid longjmp call is made, setjmp must be somewhere in the current stack context. Therefore, a longjmp that unwinds the stack and calls all destructors of auto variables, etc, appears to be logically equivalent to throwing an exception, and catching it at the point the setjmp() call was originally made.

How does throwing and catching an exception is different from your desired setjmp/longjmp semantics? If, say, you had your wished-for setjmp/longjmp implementation, then how does replacing it with an ordinary try/throw, and catching the thrown exception, be different?

The only differences that I could see is the extra inner scope introduced by the try/catch block; while setjmp does not really open a new inner scope.

So, the answer here appears to be very easy: every compliant C++ implementation has a setjmp/longjmp equivalent that has the desired semantics. It's called try/throw/catch.

Medellin answered 6/9, 2014 at 0:47 Comment(2)
My interest isn't whether there's something similar or "equivalent" to longjmp which unwinds, but whether longjmp does. See the discussion so far in comments.Particiaparticipant
No. The closest thing POSIX defines akin to unwinding is thread cancellation, and it's explicitly undefined what happens when you bypass cleanup contexts with longjmp. So an individual implementation could leave it undefined, or attempt to define a behavior for what happens then. And of course POSIX has nothing to say about the interaction with C++ exceptions since, from a POSIX perspective, C++ does not even exist.Particiaparticipant
H
0

Interix (SUA) by default does not call destructors, but in x86 mode, does have an option for it.

Taking this test program, saved as test.cc:

#include <stdio.h>
#include <setjmp.h>

struct A {
  ~A() { puts("~A"); }
};

jmp_buf buf;

void f() {
  A a;
  longjmp(buf, 1);
}

int main() {
  if (setjmp (buf))
    return 0;

  f();
}

here's how Interix behaves. For brevity, I've omitted the required proper setup of PATH.

$ cc -mx86 test.cc && ./a.out
$ cc -mx86 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
~A
$ cc -mamd64 test.cc && ./a.out
$ cc -mamd64 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
$ 

The comments suggest that cc -X /EHa does not conform to POSIX, for example because /EHa would catch signals. That is not entirely true:

$ cat test.cc
#include <signal.h>
int main() {
  try {
    raise(SIGFPE);
  } catch (...) {
    // ignore
  }
}
$ cc -mx86 -X /EHa test.cc && ./a.out
cl : Command line warning D9025 : overriding '/EHs' with '/EHa'
Floating point exception (core dumped)

If I change raise(SIGFPE) to a division by zero, I do indeed see that the exception handler catches it, but neither POSIX nor C++ requires any particular behaviour for that, so that doesn't affect conformance. It is also not the case that all asynchronous signals are caught: for this program:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void sigint(int signal) {
  puts("sigint");
  exit(0);
}
int main() {
  signal(SIGINT, sigint);
  try {
    for (;;) ;
  } catch (...) {
    // ignore
  }
}

"sigint" is printed after Ctrl-C, as expected. I don't see a reason for claiming that this implementation fails to meet POSIX requirements.

Helbonia answered 6/9, 2014 at 14:29 Comment(25)
Based on my reading of msdn.microsoft.com/en-us/library/1deeycx5.aspx, it sounds like with /EHs, the compiler just "optimizes out" the destructor path thinking that there's no way an exception can happen, and perhaps longjmp is still unconditionally doing unwinding. What happens if you add something like static volatile int x; if (x) throw(...); just before the longjmp?Particiaparticipant
@R.. One thing I had already tried was moving the longjmp to a separate function, and that had no effect: the destructor would be called only if /EHa is passed. I've now also tried your suggestion, and that too makes no difference.Helbonia
@R.. What's confusing is that the /EH* documentation seems to be saying the exact opposite of what it actually says. I think that what's happening, and I'm not entirely sure my understanding is correct, is that longjmp is implemented using SEH, and the /EHa option forces f to set up both a SEH handler and a C++ exception handler. With /EHs, only a C++ exception handler is set up. And on amd64, longjmp is not implemented using SEH.Helbonia
Interesting. Thanks for the clarifications.Particiaparticipant
IMHO SEH breaks ISO C++ standard compatibility. I don't think, that /EHa should be taken into account, because you normally implement crossplatform by using the least common denominator. It isn't even implemented platform independent inside of Interix itself. Further it conflicts with POSIX signals, because even those will be thrown and caught.Afterbody
@StefanWeiser In what way does it fail to conform to any requirement of C++? Anyway, the point of the question, as I understand it, isn't so much that you can or should rely on cleanup functions (e.g. destructors) getting called on this particular implementation, but more that you shouldn't rely on them not getting called.Helbonia
Your program isn't portable anymore, because it uses a compiler specific extension. This extension is implemented in a standard compliant way, but using it will break standard compliance for your code. However: SEH is only available on Microsoft products. It throws an exception on e.g. division-by-zero exceptions. POSIX-compliant programs would call a signal handler or terminate the program (POSIX-signals). So I don't even think of turning it on in case of developing crossplatform...Afterbody
@StefanWeiser All you're showing is that code that relies on platform-specific behaviour is platform-specific. Well of course. I never suggested otherwise.Helbonia
There are always two sides of a medal. The system must support POSIX-conformity and the program has to use it. But for the bounty, R.. is searching for a POSIX-compliant implementation (1st property), that calls destructors during unwind (2nd property). Sure, Interix supports both properties, but the 2nd one is only supported, if you compile the program in a way, where the 1st property isn't supported. So I think this is problematic. BTW even Microsoft warns their users about using SEH and /EHa for portable programs.Afterbody
@StefanWeiser It's a fair point that if cc -X /EHa is not a POSIX-conforming implementation, then it doesn't answer what the OP asked. I've yet to see evidence that it mishandles any valid program: POSIX doesn't define the interaction between signals and exception handlers, because POSIX doesn't define anything relating to C++ at all. Nonetheless, I'll run some more tests when I get the chance.Helbonia
Indeed. If @StefanWeiser is going to claim that enabling SEH breaks POSIX conformance of Interix, that claim needs to be supported.Particiaparticipant
I have no link, but it is easy to prove. If the compilation with /EHa won't break POSIX signals, then the following program would have an exit code != 0, because of an fatal error, as it does e.g. here on CentOS: ideone.com/HpOmzf. This is because POSIX defines, that the exit code is set to an value != 0 in case of an unhandled signal. Compiling it with /EHa let it return 0, because every exception is caught (the div0 exception too) as msdn.microsoft.com/en-us/library/1deeycx5.aspx claims.Afterbody
@StefanWeiser: That program invokes undefined behavior, so POSIX has nothing to say about what it does. If you change it to raise(SIGFPE), does the same thing happen?Particiaparticipant
@StefanWeiser Not only is the behaviour undefined of your program as R.. points out, it wouldn't even demonstrate your point if it were defined. POSIX allows catch(...) to have an effect on raised signals simply because it doesn't say anything whatsoever at all about C++. (I haven't tested anything yet.)Helbonia
@hvd, @R..: I guess, that we don't speak in the same terms of compatibility. A program is POSIX compatible IMO, if it will result in the same behaviour on every POSIX compatible system. You could exchange the SIGFPE with any other signal, that MS throws and therefore catches. BTW the behaviour is undefined as long as it is not raised by raise or kill. So I could update the program using raise... With the same result...Afterbody
Does it actually have the same result with raise? That's what I asked. The answer is not obvious unless you understand how Windows/Interix implements signals (which I don't). In any case, like hvd said, a C++ program can't really determine anything about POSIX conformance since POSIX does not specify the behavior of C++ programs.Particiaparticipant
Perhaps you could try a C program with pthread_cleanup_push instead of try/catch? Then it would all be in the domain that's governed by POSIX.Particiaparticipant
This whole discussion is ridiculous. Interix ships with a gcc to compile POSIX compatible. You cannot misuse a system, that has very different APIs (POSIX compatible and non compatible), by enforcing behaviour, that is clearly that of the non-compliant API, and then ask for POSIX compliance...Acescent
@R..: Yes it has the same result with raise. POSIX defines the behaviour of an OS interface. If this interface is compatible and the program is, the program should behave equal on all compliant systems. That is why people are defining portable interfaces. BTW: If I would compile a C program, how could I enforce the C++ feature SEH? I think SEH and signals are two models to handle fatal errors among others and there should be no reason to mix both (Microsoft does suggest this too in the docs on SEH).Afterbody
@Acescent No, it doesn't. It ships GCC to deal with lots of non-portable code out there that cannot compile with any other compiler than GCC. It uses CL in its own cc and c89 compilation scripts. As for non-compliance, please show proof already or stop claiming it's non-conforming. Merely re-asserting the same thing comment after comment after comment is not productive. Stefan Weiser has now claimed specific behaviour with raise, and that's testable, that's the way to convince others that they're wrong, and something I'll check when I can. (The machine isn't turned on often.)Helbonia
@StefanWeiser FWIW though, you cannot claim that a program is POSIX-compatible if it behaves the same way on all POSIX-compatible implementations, and then conclude based on the fact that one particular implementation makes it behave differently, that that implementation isn't POSIX-compatible. It would mean that either the program or the implementation isn't POSIX-compatible, but you cannot conclude which based on merely that. You've given a good example now to check against the other requirements of POSIX, and perhaps even of pure ISO C++.Helbonia
@hvd: Do you have any additional information I should look at as a basis for assigning a bounty? :-)Particiaparticipant
@R.. I may be able to run the tests based on Stefan Weiser's comments later today, but I have no additional information at this time.Helbonia
@R.. Added a few basic tests. If there's something you'd like to see tested too, please let me know.Helbonia
@hvd: I think you've provided good evidence that Interix has the properties I was asking about, so here's the +300. :-) I'll leave the answer unaccepted a bit longer in case any other interesting answers show up.Particiaparticipant

© 2022 - 2024 — McMap. All rights reserved.