📜 ⬆️ ⬇️

Implementing exceptions on plain C

Continuing this article here is habrahabr.ru/post/131212 , where I was going to show how “it is convenient to handle errors and not to use exceptions,” but they didn’t reach for it.

So, we will assume that we have a situation that “real C ++ exceptions” cannot be used - for example, the development language is C or the C ++ compiler for our platform does not support exceptions (or formally supports, but really cannot be used). This, of course, is not typical for desktop applications, but quite usually for embedded development.

We first consider the initialization of a certain subsystem, which requires (for example) three semaphores, a pair of arrays and several filters (using these arrays) that will do something there further. So:
')
//   boolean subsystem_init(subsystem* self) { //  boolean ok = true; ok = ok && sema_create(&self->sema1, 0); ok = ok && sema_create(&self->sema2, 0); ok = ok && sema_create(&self->sema3, 1); //  ok = ok && ((self->buffer1 = malloc(self->buff_size1)) != NULL); ok = ok && ((self->buffer2 = malloc(self->buff_size2)) != NULL); //  ok = ok && filter1_init(&self->f1, &self->x, &self->y, self->buffer1); ok = ok && filter2_init(&self->f2, &self->level, self->buffer2); return ok; } 


What good is this code? The most important thing is its quality - its logic is linear. There is not a single (explicit) if , the programmer simply writes the initialization sequence. Another quality of the same importance is that all errors are intercepted. Each function used in this example may fail with an error — however, information about this is not lost, but will be transferred to the upper level. Note also that in the event of an exceptional situation (for example, it was not possible to allocate memory for the buffer2 array), the system will not go into spacing (that is, there will be no attempts to create filter2 it with an invalid pointer to the buffer). In general, none of the following functions will be called, and subsystem_init will return an error upon completion. Moreover, the initialization of this subsystem can be easily integrated into the initialization of the upper level system - all that is required for this approach to be used there.

Already, in principle, everything is fine - the idea itself is extremely simple, there is no additional overhead for the implementation (there is no need to check whether the method was successfully invoked, in any case), no tricky tricks are used. The only requirement is that all functions that can be executed with an error fit into this template (it is not difficult, using the example of using malloc can see how this is done).

But we will not dwell on this, we will go further.

Suppose we tried to initialize this subsystem, but got an error. It would be nice to know exactly where the abnormal situation occurred - one of the filters did not like its parameters or, for some reason, the OS does not want to create semaphores. Boolean type here is not enough. I would like to accurately identify the problem line, and ideally - to have a normal human call stack (for example, the filter did not like the input parameters, and we have a dozen filters of the type that we don’t like — it’s not clear without a call stack).

No problem. All we need is one additional parameter for each function, something like this:
 typedef struct err_info { int count; int32_t stack[MAX_STACK]; //   ,  }; 


If an error occurs, all functions should do the following:

By convention, let all functions take this structure as the last parameter and call it the same way (for example, e ). Since the logic itself is the same, it is implemented once as a macro, all then use it. The original example will now look like this:

 //   boolean subsystem_init(subsystem* self, err_info* e) { //  REQ(sema_create(&self->sema1, 0), 0x157DF5F3); REQ(sema_create(&self->sema2, 0), 0x601414A4); REQ(sema_create(&self->sema3, 1), 0x7D8E585D); //  REQ(self->buffer1 = malloc(self->buff_size1), 0x5DEB6FC7); REQ(self->buffer2 = malloc(self->buff_size2), 0x7939EDC5); //  REQ(filter1_init(&self->f1, &self->x, &self->y, self->buffer1, e), 0x4D83E154); REQ(filter2_init(&self->f2, &self->level, self->buffer2, e), 0x5B4D8F8D); return true; } 

(ID is not generated by hands, of course, but by your favorite IDE by pressing the corresponding keys)

And the macro REQ itself can be defined, for example, as follows:
 #define REQ(X, ID) \ if (X) \ ; \ else { \ if (e->count < MAX_STACK) \ e->stack[e->count++] = ID; \ return false; \ } 


So, at the highest level, we will have the result of initialization (success / failure) and the call chain up to the very place of the error, if there was an error.

Summarizing:


ps: yes, in fact, this is a implementation of the Maybe monad.

Source: https://habr.com/ru/post/141507/


All Articles