saneex.c: try / catch / finally based on setjmp / longjmp (C99) faster than standard C ++ exceptions ¹

saneex.c: try/catch/finally based on setjmp/longjmp (C99) faster than standard C ++ exceptions ¹

While writing this purely technical article, managed to turn into a local WHO office and now I am even ashamed to publish it ... but in my heart there is a hope that IT specialists have not yet fled and it will find its reader. Or not?




I have always admired the standard C library, and C itself - for all its minimalism, they still breathe the spirit of those very first red-eyed hackers . The draft of the first official standard (ANSI C, aka C89, aka ANS X3.159-1989, aka, later, C90 and IEC 9899: 1990) defines 145 functions and macros, of which about 25 are variations (due to the lack of in the language of overloads), and 26 are purely mathematical. K&R in the second edition ² gives 114 functions (plus mathematical), considering the rest as exotic. There are already 348 functions in the draft³ C11 , but more than a hundred are mathematics, and another 90 are "overloads". Now let's look at Boost, where there are 160 libraries alone . Avoid me ...


And among this one hundred and a half functions there have always been: signal processing, variadic functions (which reached interpreted PHP 25 years later, and in Delphi, which was rapidly developing at one time, they still do not exist) and about 50 string functions like printf () ( hmm ... JavaScript), strftime () (...) and scanf () (a cheap alternative to regulars).



And there have always been setjmp ()/longjmp () , which allow you to implement the exception mechanism familiar in other languages, without going beyond the scope of portable C. Let's talk about them - Quake World , stacks, registers, assemblers and other materiel , and interesting statistics will be the cherry on top ( spoiler : Visual Studio is fickle, like a March hare, and throw

saneex.c
twice as fast as anyone).

Hidden text

¹ Based on the results of measurements in the article.


“By the way, the book is great. 270 pages, of which 80 are a summary of the standard. Either at that time they still did not know how to spread their thoughts along the tree and convert it into a fee, or the authors were above that.K&R  is old school, cho.


³ It is known from highly reliable sources that the final versions of ANSI and ISO standards are sold for money, and drafts are free. But it is not exactly.


⁴ Yes, I also do not like "shorteners" like TinyURL, but the parser considers the URL to be part of the text and swears at a long text to the kata, like Twitter pogo. This will not happen further, honestly. For the paranoid I can recommend urlex.org .


Table of contents:


  • How setjmp ()/longjmp () work
    • Registers, stack and all-all-all
    • Overwriting variables or, in Russian, clobbering
    • Volatile qualifier
    • IRL use cases
  • Performance
    • Test environment
    • results
  • The hero of the occasion - saneex.c
    • Other "features"
    • And "implementation features"
    • Bottom line: how it works

So, the heroes of our program are setjmp ()/longjmp () , defined in setjmp.h , which they like to abbreviate together as "SJLJ" (although I don't like this word, it reminds one of the notorious abbreviations). They appeared in C89 and, in general, they are not going to leave, but not everyone knows about them (knowing does not mean using - knowledge is useful, and using is as lucky).


For the sake of fairness, I must say that é already had articles on this topic, especially an excellent article from zzeng . Of course, there is also an English-language web, plus you can find implementations like this or even this ¹, but, in my opinion,they have a fatal flaw the result is either not completely familiar (for example, you cannot throw exceptions again), or mechanisms are used that are not in accordance with the standard.


¹ CException should be specially noted - only 60 lines, they write that it works quickly, also ANSI C, but it does not have finally and text messages, which is fundamentally important for me.


In general, to use exceptions or not is an eternal dispute between blunt-tips and pointed-tips in any language, and I urge those on the other side of the barricades to either walk by, or read the material and put it into their knowledge box, even if on the shelf dragged into our cozy little tit. " (The main thing is that the debaters do not forget that no C program is truly free from "exceptions", because checking errno will not save when divided by zero. Signals are the same eggs, only in profile.)


For me personally, exceptions are a tool that allows you to:


  • not to think in each specific place that something might go wrong, if this very place still cannot do anything about it (resources are not locked, memory is not allocated - you can interrupt immediately, without
    if (error) return -1;
    )
  • when something really went wrong - save as much information as possible, from the error code and file name to the values ​​of important variables and other exceptions that caused this situation

But first things first. As is customary for us , let's start with the materiel.



How setjmp ()/longjmp () work



Registers, stack and all-all-all


In a nutshell, longjmp ()  is a non-local goto , while setjmp ()  ishis propheta way to label this goto at run-time. In short, " goto on steroids". And, like any steroids, that is, goto , they can do irreparable harm to your code - turn it into such noodles that are simply out of reach for goto . Therefore, it is best to use them not directly, but inside some kind of wrapper that defines a clear hierarchy of transitions (like exceptions - up the stack within the explicitly designated "try" blocks).


Remember, I said at the beginning that from C and, specifically, from

setjmp.h
blows straight damn ^ Wunikschina? So, you call setjmp () once, and it returns as many times as you like (but at least once). Yes, in the ordinary worldsmoothies the function is called and it returns once, and in Soviet Russiathe function calls you itself, how many times it wants and when it wants to. So it goes.

This concept, by the way, is embodied not only in setjmp ()  - fork () in POSIX does something very similar. I remember when I first got acquainted with * nix APIs after ten years of working exclusively with WinAPI, it just blew me away - it didn't fit in my mental templates that functions could behave like this. As they aptly say - “what, was it possible?” ... But we were distracted.



I think everyone reading it is that the main element of the runtime is the stack on which the parameters and (some) local variables of this function lie. You call a new function - the stack grows (with Intel - down), when you exit - it melts (Intel - yes, up). Here's an example:


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

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

There is such an amusing compiler - tcc ( Tiny C Compiler ) from the famous steamer programmer F. Bellard . tcc makes almost no optimizations and the code after it is very pleasant to look at in the disassembler. It generates such a body for sub () (in Intel notation , omitting the prologue and epilogue):


sub esp, 100h; allocate space for a local variable
mov eax, 2; pass parameter
push eax
call sub_401000; call sub ()
add esp, 4; clear the stack after return (= cdecl)

Here is a diagram of what is happening with the stack:



Those orange numbers in the center are the pointer to the top of the stack (which Intel has ... well, you get the idea). The pointer is stored in the ESP register ( RSP on x86_64). setjmp () stores the current ESP/RSP value , plus other service registers, into the jmp_buf memory area that you pass to it. If longjmp () is called further down the course (from the same function or from a subfunction), the pointer is restored and it turns out that the environment of the function where setjmp () was called is automatically restored., and all previously called sub-functions are immediately completed (returned). A kind of rollback in time, "undo" for runtime (of course, with a stretch).


In the following example, setjmp () will put the pointer value into jmp

FEF8h
(
FDF0h
etc. - red arrows in the diagram above) and the function will continue execution as usual:
void sub (int s) {
  char buf [256];
  jmp_buf jmp;
  setjmp (jmp);
  sub (2);
}

But, of course, there is a nuance ™:


  • you cannot jump between threads ( setjmp () in one, longjmp () in another), because, obviously, each thread has its own stack
  • if the function that called setjmp () has already returned, then it will not work to "reanimate" it - the program will fall into undefined behavior (and this is not treated)


  • the compiler uses registers to store variables - you see, they work faster! - and the registers, suddenly, are stored separately from the stack and although setjmp () could have saved their state at the time of the call, it and longjmp () do not know what happened to them after the call to setjmp ()


Overwriting variables or, in Russian, clobbering


This last point is especially interesting. Example:


#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);
  }
}

Question to the audience: will the numbers in the console match?


Correct answer: depends on the will of the stars. So that!


Let's see what happens with gcc . If we compile with -O0 , then the numbers will match, and in the disassembler we will see this:


; int main (void) {
  push ebp; prologue (stack frame is created)
  mov ebp, esp; EBP points to the stack below the ESP (if in the scheme)
  sub esp, E0h
  ...
  call _rand; the result is returned in EAX
  mov [ebp-D4h], eax; this is i = rand (); where i on the stack (EBP-D4h)
  ...
; if (... == 0) {; call setjmp () and return from it before the jump
  call _rand
  mov [ebp-D4h], eax; again i = rand (); on the stack
; printf ("% d/n", i);
  mov eax, [ebp-D4h]; pass i from the stack as a parameter
  mov esi, eax
  lea edi, format; pass the string "% d/n"
  mov eax, 0
  call _printf
  ...
; } else {; secondary return from setjmp () after the jump
  mov eax, [ebp-D4h]; pass i again, as in the branch above
  mov esi, eax
  lea edi, format; "% d/n"
  mov eax, 0
  call _printf

As you can see, the compiler didn't bother and put the i variable on the stack (at

EBP - D4h
). If you look at the same scheme , then:
  • instead of a buffer of 256 char, we have int and jmp_buf , the size of which on my system is 4 and 200 bytes, respectively, plus 20 bytes for something was required by the compiler, so 224 bytes (E0h) were allocated on the stack for local variables instead of 100h, as in that example
  • ESP at the time of calling setjmp () is
    FFF8h - E0h = FF18h
    (instead
    FEF8h
    ), this value is stored in jmp
    • of course, this value is conditional, in reality it will be different
  • both the first assignment to i and the second change the value of i on the stack (at
    FF18h
    )
  • longjmp () flushes the stack pointer back to
    FF18h
    , but since the variable i does not go beyond these limits, it is still available, as well as the other variable ( jmp ), and the main () parameters (if they exist)
    • in this example ESP hasn't changed anyway, but longjmp () could easily be inside another function called from main ()

But if you include at least -O1 , then the picture will change:


; no more prologue and stack frame, use the ESP value directly
  sub esp, E8h
  ...
  call _rand
  mov [esp + E8h-DCh], eax; i = rand (); on the stack as with -O0
  ...
; -O1 somehow decided that else execution is more likely than
; if (setjmp () == 0) (although in my opinion the opposite), and rearranged
; their places; here I have returned the previous order for clarity
; if (... == 0) {
  call _rand
  mov esi, eax; ATTENTION! writing i to register
; printf ("% d/n", i);
  lea edi, format; "% d/n"
  mov eax, 0
  call _printf
  ...
; } else {
  mov esi, [esp + E8h-DCh]; ATTENTION! read i from the stack
  lea edi, format; "% d/n"
  mov eax, 0
  call _printf

In addition, with -O1, gcc swears at compilation with terrible words:


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

What do we see here? Initially, i is placed in a register, but in the first branch (inside if ) gcc , apparently considering i not used after the first printf () , pushes the new value directly into ESI , and not onto the stack ( it is passed on to printf () through ESI , see . ABI, page 22  - RDI ( format ), RSI ( i ), ...). Because of this:


  • on the stack at
    ESP + E8h - DCh
    the old rand () value remains
  • new value appears in ESI
  • printf () (first call) takes (new) value from register
  • longjmp () resets the stack pointer, but does not restore changed registers, which are used by functions for local variables when optimizations are enabled
  • the second call to printf () (in else ) reads the value, as expected, from the stack, that is, the old one
    • but even if it was read from ESI , then after the jump, there would be garbage in this register (probably from printf () or longjmp () itself )


Or, to rewrite it back in C:


stack [i] = rand (); //i = rand (); stack change (1)
if (setjmp (jmp) == 0) {
  ESI = rand (); //i = rand (); change case (2)
  printf ("% d/n", ESI); //print value (2)
  longjmp (jmp, 1); //jump
} else {
  printf ("% d/n", stack [i]); //print value (1)
 //or it could be like this:
  printf ("% d/n", ESI); //use case where there is already someone
                           //"visited" (first printf () or longjmp ())
}

Hidden text

To be honest, I don't understand why gcc does not immediately put the result of the first rand () into ESI or another register (even with -O3 ). On SO they write that in the x86_64 mode (for which I compiled the example), all registers are saved, except for EAX . Why store intermediate to the stack? I assumed that gcc traced printf () in the else after longjmp () , but if we remove the second rand () and this printf ()  , the result does not change, i is also pushed onto the stack first.


If anyone can shed light on this very secret - I ask in the comments.



Volatile qualifier


The solution to the problem of "volatile variables" is the volatile qualifier (literally - "volatile"). It forces the compiler to always push the variable onto the stack, so our code will work as expected at any level of optimization:


volatile int i;

The only change with -O1 will be in the if body :


; It was:
  call _rand
  mov esi, eax
; became:
  call _rand
  mov [rsp + E8h-DCh], eax
  mov esi, [rsp + E8h-DCh]
; or can be rewritten like this:
  call _rand
  mov esi, eax
  mov [rsp + E8h-DCh], eax

As you can see, the compiler has duplicated the assignment onto the stack ( compare ):


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


IRL use cases


So, if you take the precautions - not to jump between threads and between completed functions, and not to use changed non- volatile variables after the jump, then SJLJ allows us to seamlessly navigate the call stack to an arbitrary point. And you don't have to be an adherent of the exception witness sect - resistance is useless, for the SJLJ has long beenfilled the whole planet among us:


  • Wikipedia suggests using them to implement coroutines in C (I would not - no matter what happens , although ldir also mentioned such a technique )
  • on é, alexkalmuk wrote about unit tests in Elbrus based on SJLJ (+ second article ), and dzeban  - about profiling in Linux
  • also the locals wrote about a fast interpreter (careful: very strong witchcraft from Atakua ), about error handling in x86emu ( NWOcs ) and in libpng (in libjpeg-turbo it is the same )
  • in 2017 Skapix wrote about pthreads , and kutelev  about jumping from signal handlers
  • here they write that this pair was used in Symbian
  • and in Quake World it was used for sure - see the translation of the archaeological site from PatientZero and, in fact, the source (there are a couple of other places as well)

The last example, in my opinion, is the most textbook one - it is handling errors and other states when you need to exit "right now", from any level, while inserting checks to the exit everywhere is tedious, and sometimes not possible (libraries). By the way, another example was described in the DrMefistO project .


Specifically, in Quake World , an infinite loop is started in WinMain () , where each new iteration sets jmp_buf , and several functions can jump into it, thus implementing a "deep 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;
}


Performance


One of the arguments against using exceptions is their negative performance impact. Indeed, the setjmp () source in glibc shows that almost all general-purpose registers of the CPU are preserved. Nonetheless:


  • it goes without saying that neither exceptions in general nor SJLJ/
    saneex.c
    in particular and are not intended to be used in the internals of a number of crushers
  • modern te-khe ... khenology (I'm sorry, the electron got into the throat) are such that preserving an extra dozen or so registers is the smallest of the problems that they carry in themselves
  • if speed is critical, but you want exceptions, there are zero-cost exceptions (or, more precisely, zero-cost try ) mechanisms that radically reduce the load when entering a try block , leaving all the dirty work at the time of processing (throwing) - and since exceptions this is not a goto and should be used, um, in exceptional situations, then such a "skew" affects performance, uh, extremely positively

Fair zero-cost exceptions are especially useful in that they eliminate slower volatile variables that are otherwise allocated on the stack rather than in registers (which is why they are not overwritten by longjmp () ). Nevertheless, their support is already a task for the compiler and platform:


  • Windows has SEH and VEH , the latter was brought up in XP.
  • There were several different versions in gcc - first based on SJLJ, then DWARF, of which there are five versions today (DWARF is used in clang as well ). On this topic, see the excellent articles zzeng : tyts and tyts , and the site dwarfstd.org .
  • In a commentary on another article, comrade nuit gave a tip to an interesting libunwind project , but using it only for the sake of exceptions is like shooting sparrows at cannons (painfully large).

And though

saneex.c
does not pretend to be a zero-cost palm tree (its palm tree is portability), is setjmp () really as terrible as it is painted? Maybe this is superstition? In order not to be unfounded - we will measure.

Test environment


I sketched two benchmarks "on the knee", which in main () in a loop enter the try/catch block 100 thousand times and do or do not throw () .


Benchmark source in C:


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

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

C ++ source (I adapted the example from Wikipedia , taking out the vector declaration for a loop and replacing

cerr <<
on 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 {
     //either ("blowout" = yes):
      int i {vec.at (4)};
     //either ("throw away" = no):
      time (NULL);
    }
    catch (std :: out_of_range & e) {
     //<< instead of fprintf () causes the loop to slow down by 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;
}

Everything was tested on one machine in two OS (both 64-bit):


  • Windows 10 2019 LTSC under PowerShell using
    Measure-Command {test.exe 2> $ null}
  • latest Ubuntu Live CD with built-in time

I also tried to measure exceptions on Windows through the __try/__except extensions , taking another example from Wikipedia :


#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");
    }
  }
}

However, it didn't work to include the vector in the loop - the compiler reported that:


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

Since the imposed restrictions on the code go against the principle of familiarity, which I talked about at the beginning , I did not enter these results in the table below . Roughly 1100-1300ms (Debug or Release, x86) - faster than standard exceptions in VS , but still slower than they are in g ++.



results


Compiler Platform Config Mechanism Burst Time (ms) ¹ saneex slower

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

Hidden text

¹ In the Time column, added measurements of one of the readers on Windows 7 SP1 x64 with VS 2017 v15.9.17 and gcc under cygwin.


² An extremely strange fact: if fprintf () is replaced by

cerr <<
, then the execution time will be reduced by 3 times: 1386/1527 ms.

³ VS in release builds on my system gives very inconsistent results, so in further reasoning I use the reader's numbers.


The results are ... interesting:


  • Indicators float a lot on different machines and/or environments, and especially VS is "odd". What caused this is unclear.
  • Using
    cerr <<
    instead of fprintf () paired with throwing an exception in VS in a debug build, it speeds up the loop 3-4 times (line 3). CHYADNT ?
  • In all cases, the cost of a try block in the absence of a throw  is scanty (4-28 ms per 100 thousand iterations).
  • Apart from the "overclocked" Debug in VS, throwing exceptions in
    saneex.c
    faster than in built-in language constructs (2.3 times faster than VS, 5 times faster than gcc/g ++), and try without throw  is slower, but we are talking about units of milliseconds. What a twist!

What can I say ... There is something to boast about. Welcome to comments!


For me, the most important use-case is a lot of try blocks with extremely rare throws ("catch a lot, throw a little"), and it depends almost only on the speed of setjmp () , and the performance of the latter, judging by the table, is not so bad, as they often think. This is indirectly confirmed by this article , where the author, after measurements, concludes that one call to setjmp () is equal to two calls to empty functions in OpenBSD and one and a half (1.45) in Solaris. Moreover, this article is from 2005. The only "but" - you need to save without a signal mask, but it is usually not interesting.


Well, and finally ...



Hero of the occasion -
saneex.c


A library whose example was on KDPV :


  • can compile even in Visual Studio
  • supports any nesting of blocks, throw () from anywhere, finally and several catch per block (by the exception code)
  • does not allocate memory and does not use pointers (all in static )
  • optionally multithreaded ( __thread/_Thread_local )
  • in public domain ( CC0 )


Those interested can find its source on GitHub . Below I will briefly show you how to use it and what pitfalls there are with one example . Example code from

saneex-demo.c
in the repository:
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.}

The above program reads the message, throws an exception, and handles it based on user input:


  • if you do not enter anything, the exception will not be thrown, and we will see:

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

  • if you enter a text starting with one, then an exception with this code ( 1 ) will be created , it will be caught in the first block
    catch (1)
    (26.), and the screen will display:

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

  • if you enter a two, then the exception will be caught (30.), a new one will be thrown (with its own code, text, etc.) while keeping the previous information in the chain (33.), it will reach the external handler and the program will end:

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

  • if you enter
    !
    , then the exception will be "elusive" ( uncatchable ; 20.) - it will go through all try blocks higher on the stack, calling their handlers (like catch and finally ), until it reaches the external one and terminates the process - the humane analogue of 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

  • Finally, if you enter a triplet, the exception will go to catchall (35.), where its message will simply be displayed:

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


Other "features"



Thread safety. By default, it is not there, but if you have a normal compiler (not MSVC¹), then C11 will save the father of nations by placing important variables in the local stream scope ( TLS ):


#define SX_THREAD_LOCAL _Thread_local

¹ In recent years, Microsoft has made some progress on the basis of open source, butall for things are going slowly, albeit better than 8 years ago , so we are holding on .


sxTag (05.) - the string that is printed along with the uncaught exception to stderr . The default is compilation date and time ( __DATE__ __TIME__ ).


Creation of SxTraceEntry (stack trace entries). There are several useful macros - wrappers over

(struct SxTraceEntry) {...}
:
  • newex ()
     - this one was in the example ; assigns __FILE__ , __LINE__ and error code = errno (which is convenient after checking the result of a system function call, as in the example after fgets () ; 11.)
    • code less than 1 becomes 1 (because setjmp () only returns 0 on the first call), so
      catch (0)
      will never work
  • msgex (m)
     - like newex () , but also sets the error text (constant expression)
  • exex (m, e)
     - like msgex () , but also hooks an arbitrary pointer to the exception; its memory will be freed via free () automatically:

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,
   //read through void * SxTraceEntry.extra:
    ((TimeoutException *) curex (). Extra) -> elapsed);
} endtry

And, of course, there are my favorite designated initializers from the same C99 (they work in Visual Studio 2013+ ):


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

Throwing an exception:


  • throw (e)
     - throws ready SxTraceEntry
  • rethrow (e)
     - similar to throw () , but does not clear the current stack trace; can only be used inside catch/catchall
  • thrif (x, m)
     - macro; at
    if (x)
    creates an SxTraceEntry with the text x + m and "discards" it
  • thri (x)
     - like thrif () , only with empty m

Macros are needed to conveniently "convert" the result of a typical library call into an exception - as in the example with fgets () (11.), if the function could not read anything. Specifically with fgets (), this does not necessarily indicate an error (it could just be EOF :

./a.out </dev/null
), but no other suitable function is used in that example. Here's a more vital one:
thri (read (0xBaaD, buf, nbyte));
//errno = 9, "Bad file descriptor"
//Assertion error: read (0xBaaD, buf, nbyte);


... And "implementation features"


There are only two and a half of them (but what kind!):


  • the block must end with endtry  - here the process ends in the absence of a handler ( try block ) up the stack
    • the compiler will most likely catch this error, because try opens three
      {
      and endtry closes them
  • you can not do a return between try and endtry  - this is the fattest minus, but my fantasy has not found a way to catch this situation; ideas and PR are accepted
    • naturally goto inward and outward is also prohibited, but does anyone use it?
      </sarcasm>

As for the "half", this is the previously parsed volatile . The "reception" of an exception is a re-entry into the middle of the function (see longjmp () ), so if the value of a variable has been changed inside the try body , then such a variable should not be used in catch/catchall/finally and after endtry , unless it is declared as volatile . The compiler will carefully warn you about such a problem. Here's a good example:


int foo = 1;
try {
  foo = 2;
 //you can use foo here
} catchall {
 //but not here anymore!
} finally {
 //here too!
} endtry
//not allowed here too!

With volatile, a variable can be used anywhere:


volatile int foo = 1;
try {
  ...


Bottom line: how it works


Each thread has two statically allocated (global) arrays:


  • struct SxTryContext
     - information about the try blocks , inside which we are now - in particular, jmp_buf for each of them; for example, here are two of them:

try {
  try {
   //we are here
  } endtry
} endtry

  • struct SxTraceEntry
     - the current stack trace, that is, objects passed by the code from the outside to identify exceptions; there may be more or less of them than try blocks :

try {//one SxTryContext
  try {//two SxTryContext
             //zero SxTraceEntry
    throw (msgex ("First went!"));
             //one SxTraceEntry
  } catchall {
             //one SxTraceEntry
    rethrow (msgex ("The second is ready for battle!"));
             //two SxTraceEntry (*)
  } endtry
} endtry

If in the code above instead of rethrow () you use throw () , then SxTraceEntry objects

(*)
there will be not two, but one - the previous one will be deleted (stack trace will be cleared). Alternatively, you can manually add an item to the chain via
sxAddTraceEntry (e)
...

try and other constructs are macros (- your KO ). Brackets

{}
after them are optional. In the end, it all boils down to the following pseudocode:
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 {}
                                 //actions here in finally {}
} endtry remove_context ();
                                  if (! handled) {
                                   //as above with throw ()
                                  }

¹ Names with _ are not used in the library, they are abstractions.


I think after detailed explanations how SJLJ works, it is unnecessary to comment on anything else here, so let me take my leave and give the floor to you.