Ruft longjmp in C++ Destruktoren auf?


  • Administrator

    Ich konnte im C++ Standard zu longjmp nur dies hier finden:

    18.7 Other runtime support, Absatz 4 schrieb:

    The function signature longjmp(jmp_buf jbuf, int val) has more restricted behavior in this International Standard. If any automatic objects would be destroyed by a thrown exception transferring control to another (destination) point in the program, then a call to longjmp(jbuf, val) at the throw point that transfers control to the same (destination) point has undefined behavior.

    Bin daraus aber nicht schlau geworden.

    Bevor jemand fragt, wieso ich longjmp einsetze:
    Habe hier eine C Bibliothek, welche damit arbeitet. In Callbacks möchte ich mit C++ Objekten arbeiten, wobei ich darin auch wieder Funktionen aus der Bibliothek aufrufe. Diese Funktionen könnten einen longjmp auslösen.

    Grüssli



  • Also die MSDN-Seite zu longjump sagt eindeutig

    When you include setjmpex.h or setjmp.h, all calls to setjmp or longjmp will result in an unwind that invokes destructors and finally calls. This differs from x86, where including setjmp.h results in finally clauses and destructors not being invoked.

    A call to setjmp preserves the current stack pointer, non-volatile registers, and MxCsr registers. Calls to longjmp return to the most recent setjmp call site and resets the stack pointer, non-volatile registers, and MxCsr registers, back to the state as preserved by the most recent setjmp call.

    Auf cplusplus.com steht auch noch

    The function never returns to the point where it has been invoked. Instead, the function transfers the control to the point where setjmp was used to fill the env parameter.

    So würde es für mich Sinn machen, dass ein Dtor aufgerufen wird. Wenn das Scope so radikal verlassen wird (bzw. werden kann), ist das ja sogar vernünftig imo...

    Gruß
    PuerNoctis


  • Administrator

    PuerNoctis schrieb:

    Also die MSDN-Seite zu longjump sagt eindeutig

    When you include setjmpex.h or setjmp.h, all calls to setjmp or longjmp will result in an unwind that invokes destructors and finally calls. This differs from x86, where including setjmp.h results in finally clauses and destructors not being invoked.

    A call to setjmp preserves the current stack pointer, non-volatile registers, and MxCsr registers. Calls to longjmp return to the most recent setjmp call site and resets the stack pointer, non-volatile registers, and MxCsr registers, back to the state as preserved by the most recent setjmp call.

    Du solltest dir schon anschauen, was du zitierst und auch gleich die Quelle mitangeben.
    Hier die Quelle: http://msdn.microsoft.com/en-us/library/36d3b75w.aspx
    Das ist für x64 Programmierung.

    PuerNoctis schrieb:

    Auf cplusplus.com steht auch noch

    The function never returns to the point where it has been invoked. Instead, the function transfers the control to the point where setjmp was used to fill the env parameter.

    Was auf cplusplus.com steht, weiss ich auch. Aber das hilft einem überhaupt nichts dabei herauszufinden, ob eine Garantie besteht, dass die Destruktoren aufgerufen werden. Was setjmp und longjmp tun, weiss ich schon. Allerdings kenne ich es nur aus C und weiss nicht, wie die Destruktoren sich damit vertragen.

    PuerNoctis schrieb:

    So würde es für mich Sinn machen, dass ein Dtor aufgerufen wird. Wenn das Scope so radikal verlassen wird (bzw. werden kann), ist das ja sogar vernünftig imo...

    Sinn machen heisst aber nicht, dass es garantiert ist. Gibt noch vieles was Sinn machen würde und im Standard trotzdem nur als "implementation definied" drin steht.

    Wenn wir übrigens bei der MSDN bleiben, dann bekommen wir das hier:
    http://msdn.microsoft.com/en-us/library/yz2ez4as.aspx

    Es wird dringend davon abgeraten. Anscheinend geht es mit der /EH Option beim MSVC. Unten steht aber, dass man sich darauf nicht verlassen sollte, wenn man portierbaren Code schreiben möchte (was auch immer Microsoft damit genau meint). So eindeutig ist es somit nicht.

    Ich möchte nun halt gerne wissen, wie es wirklich definiert ist. Mutmassungen bringen mir nichts.

    Grüssli



  • es ist nicht möglich longjmp effizient zu implementieren wenn man alle destruktoren aufrufen will. deshalb nehme ich schwer an, dass dies UB ist.

    beim vc++ ist dies zB nicht erlaubt (also über die objektscopes zu springen):

    http://msdn.microsoft.com/en-us/library/3ye15wsy(v=VS.80).aspx schrieb:

    Be careful when using setjmp and longjmp in C++ programs. Because these functions do not support C++ object semantics, it is safer to use the C++ exception-handling mechanism.



  • "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?


  • Administrator

    Bashar schrieb:

    "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?

    Ich bin mir allerdings nicht sicher, ob ich den Teil davor richtig verstanden haben. Deshalb schrieb ich auch, dass ich daraus nicht ganz schlau geworden bin. Wenn meine Hirnzellen das richtig verarbeiten, dann steht dort, dass wenn automatische Objekte zerstört werden, weil eine Exception von A nach B geflogen ist, dann ist es undefiniert, ob diese auch zerstört werden, wenn ein longjmp von A nach B durchgeführt wird.

    (Vielleicht hoffen meine Hirnzellen auch nur einfach, dass sie dies falsch verarbeiten :D)

    Grüssli



  • Nicht ganz. Es nicht nicht undefiniert, ob die Objekte zerstört werden. Das ganze Verhalten ist undefiniert.
    Spekulation: Wahrscheinlich hätte man sagen können, dass es unspezifiziert ist, ob die Objekte zerstört werden. Oder sogar, dass sie nicht zerstört werden. Da in solchen Fällen allerdings das ganze Lebenszyklus-Modell eines Objektes zusammenbricht dürfte man sich auf die sichere Ebene "undefiniert" (sprich: longjmp nur in reinem C bitte, wenn es gar nicht anders geht) geeinigt haben.


  • Mod

    Dravere schrieb:

    Bashar schrieb:

    "has undefined behaviour" ist doch hinreichend eindeutig, oder nicht?

    Ich bin mir allerdings nicht sicher, ob ich den Teil davor richtig verstanden haben. Deshalb schrieb ich auch, dass ich daraus nicht ganz schlau geworden bin. Wenn meine Hirnzellen das richtig verarbeiten, dann steht dort, dass wenn automatische Objekte zerstört werden, weil eine Exception von A nach B geflogen ist, dann ist es undefiniert, ob diese auch zerstört werden, wenn ein longjmp von A nach B durchgeführt wird.

    (Vielleicht hoffen meine Hirnzellen auch nur einfach, dass sie dies falsch verarbeiten :D)

    Grüssli

    fast. Dort steht, dass wenn automatische Objekte zerstört werden würden (d.h. nicht-PODs, denn PODs werden nicht zerstört sondern nur freigegeben), falls eine Exception per throw von A nach B fliegen würde, dann löst die Verwendung von longjmp zum gleichen Zielpunkt B an Stelle des throws undefiniertes Verhalten aus.
    Es steht nicht da, dass Destruktoren nicht ausgeführt werden: das würde nämlich das Verhalten spezifizieren und gerade nicht undefiniert lassen.

    Das ganze ist deshalb so umständlich formuliert, weil longjmp natürlich immer noch dort funktionieren soll, wo im Grunde nur reines C verwendet wird.


  • Administrator

    Danke, bestätigt somit meine Befürchtungen endgültig.

    Mal sehen, wie ich das nun am besten löse...

    Grüssli



  • Wahrscheinlich musst du an den kritischen Stellen (die Zeit, in der das böse longjmp() aufgerufen werden könnte) auf reines C zurückgreifen, zumindest was die Objektsemantik betrifft. Andere C++-Features wie Templates kannst du ja nach wie vor verwenden. Vielleicht besteht eine Möglichkeit darin, C++-Objekte mit einem C-Interface zu wrappen, wie es in Bindings getan wird. Dann musst du die Wrapper-Objekte zwar manuell konstruieren und zerstören, aber kannst intern normal arbeiten.

    Das Problem ist aber nicht nur ein C++-spezifisches. Wie löst man es in C, wenn eine aufgerufene Funktion den Programmkontext neu setzen kann? Falls die Kontrolle nicht direkt zum Aufrufer zurückgelangt, wird es mit Speicherfreigabe etc. unter Umständen schwierig. Überhaupt stelle ich mir das Arbeiten mit setjmp() und longjmp() ziemlich übel vor...


  • Administrator

    Nexus schrieb:

    Wahrscheinlich musst du an den kritischen Stellen (die Zeit, in der das böse longjmp() aufgerufen werden könnte) auf reines C zurückgreifen, zumindest was die Objektsemantik betrifft. Andere C++-Features wie Templates kannst du ja nach wie vor verwenden. Vielleicht besteht eine Möglichkeit darin, C++-Objekte mit einem C-Interface zu wrappen, wie es in Bindings getan wird. Dann musst du die Wrapper-Objekte zwar manuell konstruieren und zerstören, aber kannst intern normal arbeiten.

    Aktuell sieht es so aus, dass ich womöglich folgendes machen kann:

    begin callback
    
     some longjmp danger code
    
     normal c++ code without longjmp
    
     some longjmp danger code
    
    end callback
    

    So könnte ich eine Funktion aufrufen, welche dann den C++ Code enthält oder halt einfach einen zusätzlichen Block einführen.

    Nexus schrieb:

    Das Problem ist aber nicht nur ein C++-spezifisches. Wie löst man es in C, wenn eine aufgerufene Funktion den Programmkontext neu setzen kann? Falls die Kontrolle nicht direkt zum Aufrufer zurückgelangt, wird es mit Speicherfreigabe etc. unter Umständen schwierig. Überhaupt stelle ich mir das Arbeiten mit setjmp() und longjmp() ziemlich übel vor...

    Naja, grundsätzlich gleich wie mit Exceptions. Gewisse Kompiler verwenden setjmp und longjmp um Exceptions umzusetzen. In C muss man halt viel Code selber hinschreiben, welcher der Kompiler in C++ hinschreiben würde. In C besteht somit die Gefahr, dass man was vergisst. Man kann da auch mit Makros rumtricksen, aber auch da muss man dann auf gewisse Dinge achten. Möglich ist es definitiv und kann unter Umständen auch die Fehlerbehandlung etwas erleichtern. Also ellenlange if-else Prüfungen vernichten.

    Aber es ist definitiv Vorsicht geboten 🙂

    Grüssli



  • Dravere schrieb:

    So könnte ich eine Funktion aufrufen, welche dann den C++ Code enthält oder halt einfach einen zusätzlichen Block einführen.

    Ja, das ist natürlich das Einfachste, wenn die bösen Funktionen nicht auf Daten deiner C++-Objekte zugreifen und dabei unerwartete Sprünge aus deren Scope durchführen.

    Dravere schrieb:

    Aber es ist definitiv Vorsicht geboten 🙂

    C++-Exceptions passen vor allem sehr gut zu RAII. Ohne automatische Zerstörung würde man den grossen Vorteil der Exceptions einbüssen, nicht auf jeder Ebene der Aufrufhierarchie Fehlerbehandlung und Aufräumaktionen durchführen zu müssen.

    Aber RAII kann ja longjmp() nicht. Wie kann man also Speicher freigeben, wenn eine Funktion den Scope zu verlassen droht? Einen Callback einrichten und jeweils neu mitgeben? An so eine Situation denke ich:

    // C++
    Obj obj;
    FunktionMitThrow(); // kein Problem
    
    // C
    Obj* obj = CreateObj();
    FunktionMitLangemSprung();
    DestroyObj(obj); // evtl. zu spät
    

    Sorry, falls das etwas OffTopic ist. Vielleicht sollte ich die Frage auch im C-Forum stellen... 🙂


  • Administrator

    Nexus schrieb:

    C++-Exceptions passen vor allem sehr gut zu RAII. Ohne automatische Zerstörung würde man den grossen Vorteil der Exceptions einbüssen, nicht auf jeder Ebene der Aufrufhierarchie Fehlerbehandlung und Aufräumaktionen durchführen zu müssen.

    Aber RAII kann ja longjmp() nicht. Wie kann man also Speicher freigeben, wenn eine Funktion den Scope zu verlassen droht? Einen Callback einrichten und jeweils neu mitgeben? An so eine Situation denke ich:

    // C++
    Obj obj;
    FunktionMitThrow(); // kein Problem
    
    // C
    Obj* obj = CreateObj();
    FunktionMitLangemSprung();
    DestroyObj(obj); // evtl. zu spät
    

    Sorry, falls das etwas OffTopic ist. Vielleicht sollte ich die Frage auch im C-Forum stellen... 🙂

    Du kannst dies grundsätzlich einfach über ein künstliches finally erreichen. Du musst dazu natürlich jmp_buf kapseln und immer wenn du ein neuer jmp_buf einführst, musst du den vorherigen speichern. Also grundsätzlich einen zusätzlichen Stack mitführen.

    Also etwas pseudomässig:

    int doRethrow = 0;
    Obj* obj = CreateObj();
    
    push_jmp_frame(); // thread_jmp_frame verweist auf ein neues Objekt.
    
    if(setjmp(thread_jmp_frame->buf) == 0)
    {
      // try
      FunktionMitLangemSprung();
    }
    else
    {
      // catch
      doRethrow = 1;
    }
    
    pop_jmp_frame();
    
    // finally
    DestroyObj(obj);
    
    if(doRethrow)
    {
      rethrow();
    }
    

    Irgendetwas in der Art.

    Grüssli



  • Ah, klingt interessant, danke. Mit Makros könnte man das sicher noch "benutzerfreundlich" machen. 😉

    Ich sehe schon, ich muss wieder mal in Ruhe experimentieren... 🕶


  • Administrator

    Nexus schrieb:

    Ah, klingt interessant, danke. Mit Makros könnte man das sicher noch "benutzerfreundlich" machen. 😉

    Jein, darüber kann man ziemlich streiten. Ich habe bis heute keine Implementation gesehen, wo du dann im TRY-CATCH-FINALLY Bereich einfach ein return hinsetzen konntest. Wegen den Makros wird dies dann sogar versteckt und es steht nur in der Dokumentation, dass dies nicht geht. Das ist natürlich sehr gefährlich, so hast du plötzlich einen Frame nicht entfernt. Dieser Fehler fällt womöglich auch nicht sofort auf, bis irgendwann ein longjmp bis an den Anfang zurückgehen soll. Der Fehler bleibt also vielleicht Jahre lang unentdeckt, die Software läuft Jahre lang ... und dann plötzlich passiert ein äusserst seltsamer Fehler, welche garantiertes undefiniertes Verhalten auslöst und die ganze angeschlosssene Datenbank löscht - Licht aus 🤡

    Also mal ganz unter uns C++'ler:
    Bin ich froh Exceptions und RAII zu haben! 😃

    Grüssli


Anmelden zum Antworten