saneex.c: try / catch / finally basierend auf setjmp / longjmp (C99) schneller als Standard-C ++ - Ausnahmen ¹

Während Habr diesen rein technischen Artikel schrieb, gelang es ihm, sich in das lokale Büro der WHO zu verwandeln, und jetzt schäme ich mich sogar, ihn zu veröffentlichen ... aber die Hoffnung erwärmt sich in der Seele, dass IT-Leute noch nicht geflohen sind und sie ihren Leser finden wird. Oder nicht?




Ich wurde immer von der Standard-C-Bibliothek bewundert, und C selbst - mit all seinem Minimalismus bläst es immer noch den Geist derer, die es zuerst haben rotäugig Hacker . Der Entwurf der ersten offiziellen Norm (ANSI C, alias C89, alias ANS X3.159-1989, später C90 und IEC 9899: 1990) definiert 145 Funktionen und Makros, von denen etwa 25 Variationen sind (aufgrund des Fehlens von in der Sprache der Überladungen) und 26 sind rein mathematisch. K & R zitiert in der zweiten Ausgabe ² 114 Funktionen (plus mathematische), wobei der Rest als exotisch betrachtet wird. Im Entwurf³ C11 gibt es bereits 348 Funktionen, aber mehr als hundert sind Mathematik und 90 weitere sind „Überladungen“. Schauen wir uns nun Boost an, wo es nur 160 Bibliotheken gibt . Chur mich ...


Und unter diesen hunderteinhalb Funktionen gab es immer: Signalverarbeitung, Variationsfunktionen (die 25 Jahre später das interpretierte PHP erreichten, und in Delphi, das zu einer Zeit boomte, fehlen sie immer noch) und ungefähr 50 Zeichenfolgenfunktionen wie printf () ( mmm ... JavaScript), strftime () (...) und scanf () (eine günstige Alternative zu Stammgästen).



Und es gab schon immer setjmp () / longjmp () , mit denen Sie den in anderen Sprachen bekannten Ausnahmemechanismus implementieren können, ohne über den Bereich von portablem C hinauszugehen. Hier wird über sie gesprochen - die Quake World , Stapel, Register, Assembler und anderes Material , und Kirsche ist eine merkwürdige Statistik ( Spoiler : Visual Studio ist als Märzhase nicht konstant und wirft die saneex.c Hälfte der Zeit ein).



Versteckter Text

¹ .


² , . 270 , 80 — . , . K&R — , .


³ , ANSI ISO , . .


⁴ , «» TinyURL, URL , і . , -. urlex.org.


Inhaltsverzeichnis:



,  — setjmp()/longjmp(), setjmp.h, «SJLJ» ( , ). C89 , -, , (  — ,  — ).


, , , zzeng. , , , ¹, , , ( , ), .


¹ CException  — 60 , , , ANSI C, finally , .


,  — , , , , , « ». (, , - «» , errno .  — , .)


, :


  • , - , ( ,  — , if (error) return -1;)
  • -  — , ,

. , .



setjmp()/longjmp()



, --


, longjmp() — goto, setjmp() — goto run-time. , «goto ». , , , goto,  — , goto . , - , (  — «try»).


, , , , setjmp.h ^W ? , setjmp() , (, , ). , , , . .


, , setjmp() — fork() POSIX . , *nix’ API WinAPI,  — , .  — « , ?»… .



, ,  — , () .  — ( Intel’ — ),  — ( Intel’ — -, ). :


void sub(int s) {
  char buf[256];
  sub(2);
}

int main(int m) {
  sub(1);
}

 — tcc (Tiny C Compiler) - . . tcc . sub() ( Intel, ):


sub     esp, 100h       ;     
mov     eax, 2          ;  
push    eax
call    sub_401000      ;  sub()
add     esp, 4          ;     (= cdecl)

:



 — ( Intel… , ). ESP (RSP x86_64). setjmp() ESP/RSP, , jmp_buf, . longjmp() ( ) — , , setjmp(), (). , «undo» (, ).


setjmp() jmp FEF8h (FDF0h .. — ) , :


void sub(int s) {
  char buf[256];
  jmp_buf jmp;
  setjmp(jmp);
  sub(2);
}

, , ™:




  •  — , , ! — , , , setjmp() , longjmp() , setjmp()


, -, clobbering


. :


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

int main(void) {
  int i;
  jmp_buf jmp;
  i = rand();
  if (setjmp(jmp) == 0) {
    i = rand();
    printf("%d\n", i);
    longjmp(jmp, 1);
  } else {
    printf("%d\n", i);
  }
}

: ?


: . -!


, gcc. -O0, , :


; int main(void) {
  push    ebp             ;  ( stack frame)
  mov     ebp, esp        ; EBP     ESP (  )
  sub     esp, E0h
  ...
  call    _rand           ;    EAX
  mov     [ebp-D4h], eax  ;  i = rand();  i   (EBP-D4h)
  ...
; if (... == 0) {         ;  setjmp()      
  call    _rand
  mov     [ebp-D4h], eax  ;  i = rand();  
; printf("%d\n", i);
  mov     eax, [ebp-D4h]  ;  i    
  mov     esi, eax
  lea     edi, format     ;   "%d\n"
  mov     eax, 0
  call    _printf
  ...
; } else {                ;    setjmp()  
  mov     eax, [ebp-D4h]  ;   i,    
  mov     esi, eax
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf

, i ( EBP - D4h). , :


  • 256 char int jmp_buf, 4 200 , 20 - , 224 (E0h) 100h,
  • ESP setjmp() FFF8h - E0h = FF18h ( FEF8h), jmp
    • , ,
  • i, i ( FF18h)
  • longjmp() FF18h, , i , - , (jmp), main() ( )
    • ESP , longjmp() , main()

-O1, :


;   stack frame  ,   ESP 
  sub     esp, E8h
  ...
  call    _rand
  mov     [esp+E8h-DCh], eax  ; i = rand();  ,    -O0
  ...
; -O1 - ,   else  , 
; if (setjmp() == 0) ( - ),  
;  ;       
; if (... == 0) {
  call    _rand
  mov     esi, eax            ; !  i  
; printf("%d\n", i);
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf
  ...
; } else {
  mov     esi, [esp+E8h-DCh]  ; !  i  
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf

, -O1 gcc :


test.c:6:11: warning: variable ‘i’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered]

? i , ( if) gcc, i printf(), ESI, ( ESI printf(), . ABI, . 22 — RDI (format), RSI (i), …). - :


  • ESP + E8h - DCh rand()
  • ESI
  • printf() ( ) ()
  • longjmp() , ,
  • printf() ( else) , , ,
    • ESI, (, printf() longjmp())


, :


stack[i] = rand();          // i = rand();   (1)
if (setjmp(jmp) == 0) {
  ESI = rand();             // i = rand();   (2)
  printf("%d\n", ESI);      //   (2)
  longjmp(jmp, 1);          // 
} else {
  printf("%d\n", stack[i]); //   (1)
  //     :
  printf("%d\n", ESI);      //  ,   -
                            // "" ( printf()  longjmp())
}

, , gcc rand() ESI ( -O3). SO , x86_64 ( ) , EAX. ? , gcc printf() else longjmp(), rand() printf() — , i .


 — .



volatile


« » — volatile ( — «»). , , , :


volatile int i;

-O1 if:


; :
  call    _rand
  mov     esi, eax
; :
  call    _rand
  mov     [rsp+E8h-DCh], eax
  mov     esi, [rsp+E8h-DCh]
;    :
  call    _rand
  mov     esi, eax
  mov     [rsp+E8h-DCh], eax

, ():


if (setjmp(jmp) == 0) {
  ESI = stack[i] = rand();


IRL


,  — -volatile , SJLJ .  — , SJLJ :



, ,  — , « », , , - (). , DrMefistO.


Quake World WinMain(), jmp_buf, , « continue»:


// WinQuake/host.c
jmp_buf         host_abortserver;

void Host_EndGame (char *message, ...)
{
  ...

  if (cls.demonum != -1)
    CL_NextDemo ();
  else
    CL_Disconnect ();

  longjmp (host_abortserver, 1);
}

void Host_Error (char *error, ...)
{
  ...

  if (cls.state == ca_dedicated)
    Sys_Error ("Host_Error: %s\n",string);  // dedicated servers exit

  CL_Disconnect ();
  cls.demonum = -1;

  inerror = false;

  longjmp (host_abortserver, 1);
}

void _Host_Frame (float time)
{
  static double           time1 = 0;
  static double           time2 = 0;
  static double           time3 = 0;
  int                     pass1, pass2, pass3;

  if (setjmp (host_abortserver) )
    return;                 // something bad happened, or the server disconnected

  ...
}

// QW/client/sys_win.c
int WINAPI WinMain (...)
{
  ...

  while (1)
  {
    ...
    newtime = Sys_DoubleTime ();
    time = newtime - oldtime;
    Host_Frame (time);
    oldtime = newtime;
  }

  /* return success of application */
  return TRUE;
}



,  — . , setjmp() glibc , . :


  • , , SJLJ/saneex.c
  • -…- (, ) , -  — ,
  • ,  — zero-cost exceptions (, , zero-cost try), try, () — goto , , , «» , -,

«» zero-cost exceptions , volatile-, , ( longjmp()). , :



, saneex.c zero-cost (  — ), setjmp(), ? , ?  — .




« », main() 100 try/catch throw().


C:


#include <stdio.h>
#include <time.h>
#include "saneex.h"

int main(void) {
  for (int i = 0; i < 100000; i++) {
    try {
      //  ("" = ):
      throw(msgex("A quick fox jumped over a red dog and a nyancat was spawned"));
      //  ("" = ):
      time(NULL);
    } catchall {
      fprintf(stderr, "%s\n", curex().message);
    } endtry
  }
}

++ ( , cerr << fprintf()):


#include <iostream>
#include <vector>
#include <stdexcept>
#include <time.h>

int main() {
  std::vector<int> vec{ 3, 4, 3, 1 };

  for (int i = 0; i < 100000; i++) {
    try {
      //  ("" = ):
      int i{ vec.at(4) };
      //  ("" = ):
      time(NULL);
    }
    catch (std::out_of_range & e) {
      // <<  fprintf()     25-50%
      //std::cerr << "Accessing a non-existent element: " << e.what() << '\n';
      fprintf(stderr, "%s\n", e.what());
    }
    catch (std::exception & e) {
      //std::cerr << "Exception thrown: " << e.what() << '\n';
      fprintf(stderr, "%s\n", e.what());
    }
    catch (...) {
      //std::cerr << "Some fatal error\n";
      fprintf(stderr, "Some fatal error");
    }
  }

  return 0;
}

( 64-):


  • Windows 10 2019 LTSC PowerShell Measure-Command { test.exe 2>$null }
  • Live CD Ubuntu time

Windows __try/__except, :


#include <windows.h>
#include <stdio.h>
#include <vector>

int filterExpression(EXCEPTION_POINTERS* ep) {
  ep->ContextRecord->Eip += 8;
  return EXCEPTION_EXECUTE_HANDLER;
}

int main() {
  static int zero;
  for (int i = 0; i < 100000; i++) {
    __try {
      zero = 1 / zero;
      __asm {
        nop
        nop
        nop
        nop
        nop
        nop
        nop
      }
      printf("Past the exception.\n");
    }
    __except (filterExpression(GetExceptionInformation())) {
      printf("Handler called.\n");
    }
  }
}

 — , :


error C2712: Cannot use __try in functions that require object unwinding

, , . 1100-1300 (Debug Release, x86) — , VS, , g++.




№                     ()¹           saneex 

1.  VS 2019 v16.0.0   Debug     x64   saneex.c        9713  / 8728  = 1.1    1.8 / 1.8
2.  VS 2019 v16.0.0   Debug     x64   saneex.c       95    / 46    = 2      4.5 / 2.3
3.  VS 2019 v16.0.0   Debug     x64   C++             5449  / 4750² = 1.6
4.  VS 2019 v16.0.0   Debug     x64   C++            21    / 20    = 1
5.  VS 2019 v16.0.0   Release   x64   saneex.c        8542³ / 182   = 47     1.8 / 0.4
6.  VS 2019 v16.0.0   Release   x64   saneex.c       80³   / 23    = 3.5    8   / 1.8
7.  VS 2019 v16.0.0   Release   x64   C++             4669³ / 420   = 11
8.  VS 2019 v16.0.0   Release   x64   C++            10³   / 13    = 0.8
9.  gcc 9.2.1         -O0       x64   saneex.c        71    / 351   = 0.2    0.2 / 0.6
10. gcc 9.2.1         -O0       x64   saneex.c       6     / 39    = 0.2    1.5 / 1.1
11. g++ 9.2.1         -O0       x64   C++             378   / 630   = 0.6
12. g++ 9.2.1         -O0       x64   C++            4     / 37    = 0.1
13. gcc 9.2.1         -O3       x64   saneex.c        66    / 360   = 0.2    0.2 / 0.6
14. gcc 9.2.1         -O3       x64   saneex.c       5     / 23    = 0.2    1   / 0.6
15. g++ 9.2.1         -O3       x64   C++             356   / 605   = 0.6
16. g++ 9.2.1         -O3       x64   C++            5     / 38    = 0.1

¹ Windows 7 SP1 x64 VS 2017 v15.9.17 gcc cygwin.


² : fprintf() cerr <<, 3 : 1386/1527 .


³ VS , .


… :


  • / «» VS.  — .
  • cerr << fprintf() VS 3-4 ( 3). ?
  • try throw — (4-28 100 ).
  • «» Debug VS, saneex.c , ( 2.3 VS, 5 gcc/g++), try throw — , . !

… . !


use-case — try throw (« , »), setjmp(), , , , . , , setjmp() OpenBSD (1.45) — Solaris. 2005 . «» — , .


, …



 — saneex.c


, :


  • Visual Studio
  • , throw() , finally catch ( )
  • ( static)
  • - (__thread/_Thread_local)
  • public domain (CC0)


GitHub. , . saneex-demo.c :


01.    #include <stdio.h>
02.    #include "saneex.h"
03.
04.    int main(void) {
05.      sxTag = "SaneC's Exceptions Demo";
06.
07.      try {
08.        printf("Enter a message to fail with: [] [1] [2] [!] ");
09.
10.        char msg[50];
11.        thrif(!fgets(msg, sizeof(msg), stdin), "fgets() error");
12.
13.        int i = strlen(msg) - 1;
14.        while (i >= 0 && msg[i] <= ' ') { msg[i--] = 0; }
15.
16.        if (msg[0]) {
17.          errno = atoi(msg);
18.          struct SxTraceEntry e = newex();
19.          e = sxprintf(e, "Your message: %s", msg);
20.          e.uncatchable = msg[0] == '!';
21.          throw(e);
22.        }
23.
24.        puts("End of try body");
25.
26.      } catch (1) {
27.        puts("Caught in catch (1)");
28.        sxPrintTrace();
29.
30.      } catch (2) {
31.        puts("Caught in catch (2)");
32.        errno = 123;
33.        rethrow(msgex("calling rethrow() with code 123"));
34.
35.      } catchall {
36.        printf("Caught in catchall, message is: %s\n", curex().message);
37.
38.      } finally {
39.        puts("Now in finally");
40.
41.      } endtry
42.
43.      puts("End of main()");
44.    }

, :


  •  — , :

End of try body
Now in finally
End of main()

  • , , (1), catch (1) (26.), :

Caught in catch (1)
Your message: 1 hello, habr!
    ...at saneex-demo.c:18, code 1
Now in finally
End of main()

  • , (30.), ( , ) (33.), :

Caught in catch (2)
Now in finally

Uncaught exception (code 123) - terminating. Tag: SaneC's Exceptions Demo
Your message: 2 TM! kak tam blok4ain?
    ...at saneex-demo.c:18, code 2
calling rethrow() with code 123
    ...at saneex-demo.c:33, code 123
rethrown by ENDTRY
    ...at saneex-demo.c:41, code 123

  • !, «» (uncatchable; 20.) — try , ( catch, finally),  — abort():

Caught in catch (1)
Your message: ! it is a good day to die
    ...UNCATCHABLE at saneex-demo.c:18, code 0
Now in finally

Uncaught exception (code 0) - terminating. Tag: SaneC's Exceptions Demo
Your message: ! it is a good day to die
    ...UNCATCHABLE at saneex-demo.c:18, code 0
UNCATCHABLE rethrown by ENDTRY
    ...at saneex-demo.c:41, code 0

  • , , catchall (35.), :

Caught in catchall, message is: Your message: 3 we need more gold
Now in finally
End of main()


«»



. , ( MSVC¹), C11 (TLS):


#define SX_THREAD_LOCAL _Thread_local

¹ Microsoft - open source, , , 8 , .


sxTag (05.) — , stderr.  — (__DATE__ __TIME__).


SxTraceEntry ( stack trace).  — (struct SxTraceEntry) {...}:


  • newex() — ; __FILE__, __LINE__ = errno ( , fgets(); 11.)
    • 1 1 ( setjmp() 0 ), catch (0)
  • msgex(m) — newex(), ( )
  • exex(m, e) — msgex(), ; free() :

try {
  TimeoutException *e = malloc(sizeof(*e));
  e->elapsed = timeElapsed;
  e->limit = MAX_TIMEOUT;
  errno = 146;
  throw(exex("Connection timed out", e));
} catch (146) {
  printf("%s after %d\n", curex().message,
    //   void *SxTraceEntry.extra:
    ((TimeoutException *) curex().extra)->elapsed);
} endtry

, , designated initializers C99 ( Visual Studio 2013+):


throw( (struct SxTraceEntry) {.message = "kaboom!"} );

:


  • throw(e) — SxTraceEntry
  • rethrow(e) — throw(), stack trace; catch/catchall
  • thrif(x, m) — ; if (x) SxTraceEntry x + m «»
  • thri(x) — thrif(), m

«»  — fgets() (11.), . fgets() ( EOF: ./a.out </dev/null), . :


thri(read(0xBaaD, buf, nbyte));
// errno = 9, "Bad file descriptor"
// Assertion error: read(0xBaaD, buf, nbyte);


… « »


( !):


  • endtry — ( try)
    • , , , try {, endtry
  • return try endtry — , ; PR
    • , goto , - ? </sarcasm>

«», volatile. «»  — (. longjmp()), , try, catch/catchall/finally endtry, volatile. . :


int foo = 1;
try {
  foo = 2;
  //    foo
} catchall {
  //    !
} finally {
  //   !
} endtry
//   !

volatile :


volatile int foo = 1;
try {
  ...


:


- () :


  • struct SxTryContext — try,  — , jmp_buf ; , :

try {
  try {
    //  
  } endtry
} endtry

  • struct SxTraceEntry — stack trace, , ; , try:

try {         //  SxTryContext
  try {       //  SxTryContext
              //  SxTraceEntry
    throw(msgex(" !"));
              //  SxTraceEntry
  } catchall {
              //  SxTraceEntry
    rethrow(msgex("   !"));
              //  SxTraceEntry (*)
  } endtry
} endtry

rethrow() throw(), SxTraceEntry (*) ,  — (stack trace ). , sxAddTraceEntry(e).


try und andere Strukturelemente sind Makros (- Ihr K.O. ). Klammern { }danach sind optional. Am Ende läuft alles auf den folgenden Pseudocode hinaus:


try {                             int _sxLastJumpCode = setjmp(add_context()¹);
                                  bool handled = false;
                                  if (_sxLastJumpCode == 0) {
  throw(msgex("Mama mia!"));        clearTrace();
                                    sxAddTraceEntry(msgex(...));
                                    if (count_contexts() == 0) {
                                      fprintf(stderr, "Shurik, vs propalo!");
                                      sxPrintTrace();
                                      exit(curex().code);
                                    } else {
                                      longjmp(top_context());
                                    }
} catch (9000) {                  } else if (_sxLastJumpCode == 9000) {
                                    handled = true;
} catchall {                      } else {
                                    handled = true;
} finally {                       }
                                  //    finally { }
} endtry                          remove_context();
                                  if (!handled) {
                                    //    throw()
                                  }

¹ Die Namen mit _ werden in der Bibliothek nicht verwendet, sondern sind Abstraktionen.


Ich denke, nach detaillierten Erklärungen zur Funktionsweise von SJLJ ist hier nichts anderes zu kommentieren. Lassen Sie mich also Urlaub nehmen und Ihnen bereits das Wort erteilen.


All Articles