Implementing exceptions in plain C
The continuation of this article habrahabr.ru/post/131212 , where I was going to show how "it’s convenient to handle errors and do not use exceptions at the same time", but they didn’t get their hands on 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 it, but you really can't use it). This, of course, is not typical for desktop applications, but it is quite common for embedded development.
First, consider the initialization of a subsystem that needs (for example) three semaphores, a pair of arrays and several filters (using these arrays) that will do something further there. So:
What is this code good for? Most importantly, its quality is its logic is linear. There is not a single (explicit) if in it , 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 - however, information about this is not lost, but will be transferred to the upper level. Note also that if an exception occurs (for example, it was not possible to allocate memory for the array
Already, in principle, everything is fine - the idea itself is extremely simple, there is no additional overhead for the implementation (it is necessary to check whether the method was successfully called, in any case), no tricky tricks are used. The only requirement is that all functions that may be executed with an error fit into this template (this is not difficult, the use case
But we will not stop here, we will go further.
Suppose we tried to initialize this subsystem, but got an error. It would be nice to know exactly wherean emergency occurred - one of the filters didn’t like its parameters or for some reason the OS does not want to create semaphores. The Boolean type is not enough here. I want to accurately identify the problem line, and ideally, have a normal human call stack (for example, the filter didn’t like the input parameters, and we have a dozen filters of the type that we didn’t like exactly - it’s not clear without the call stack).
Not a problem. All we need is one additional parameter for each function, something like this:
In the event of an error, all functions must do the following:
By convention, let all functions take this structure as the last parameter and it will be called identically (for example, e ). Since the logic itself is the same, it is implemented once in the form of a macro, all then use it. The original example will now look like this:
(The ID is generated not by hand, of course, but by the favorite IDE by pressing the appropriate keys)
And the REQ macro itself can be defined, for example, in this way:
In total, at the highest level, we will have the initialization result (success / failure) and a chain of calls up to the very place where the error occurred, if there was an error.
In summary:
ps: yes, in fact - this is the implementation of the Maybe monad.
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 it, but you really can't use it). This, of course, is not typical for desktop applications, but it is quite common for embedded development.
First, consider the initialization of a subsystem that needs (for example) three semaphores, a pair of arrays and several filters (using these arrays) that will do something further there. 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 is this code good for? Most importantly, its quality is its logic is linear. There is not a single (explicit) if in it , 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 - however, information about this is not lost, but will be transferred to the upper level. Note also that if an exception occurs (for example, it was not possible to allocate memory for the array
buffer2
), the system will not go into spacing (i.e. there will be no attempts to create filter2
an invalid pointer to the buffer by slipping it). In general, none of the following functions will be called, butsubsystem_init
upon completion, it will return an error. Moreover, the initialization of this subsystem can be easily integrated into the initialization of a top-level system - all that is required for this approach to be used there as well. Already, in principle, everything is fine - the idea itself is extremely simple, there is no additional overhead for the implementation (it is necessary to check whether the method was successfully called, in any case), no tricky tricks are used. The only requirement is that all functions that may be executed with an error fit into this template (this is not difficult, the use case
malloc
shows how this is done). But we will not stop here, we will go further.
Suppose we tried to initialize this subsystem, but got an error. It would be nice to know exactly wherean emergency occurred - one of the filters didn’t like its parameters or for some reason the OS does not want to create semaphores. The Boolean type is not enough here. I want to accurately identify the problem line, and ideally, have a normal human call stack (for example, the filter didn’t like the input parameters, and we have a dozen filters of the type that we didn’t like exactly - it’s not clear without the call stack).
Not a 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]; // стек не бесконечный, да
};
In the event of an error, all functions must do the following:
- add a unique ID to the stack (if there is still space),
- exit the function by returning
false
.
By convention, let all functions take this structure as the last parameter and it will be called identically (for example, e ). Since the logic itself is the same, it is implemented once in the form of 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;
}
(The ID is generated not by hand, of course, but by the favorite IDE by pressing the appropriate keys)
And the REQ macro itself can be defined, for example, in this way:
#define REQ(X, ID) \
if (X) \
; \
else { \
if (e->count < MAX_STACK) \
e->stack[e->count++] = ID; \
return false; \
}
In total, at the highest level, we will have the initialization result (success / failure) and a chain of calls up to the very place where the error occurred, if there was an error.
In summary:
- Remained within ANSI C (didn’t use any non-standard extensions),
- received (almost free) call stack,
- exceptional situations are caught,
- the code is linear, it is not littered with constant checks of return values, which are easy to make mistakes,
- if we have “C ++ without exceptions”, we get calls to destructors of already created local variables in the same way, as with the usual generation of exceptions.
ps: yes, in fact - this is the implementation of the Maybe monad.