I am working on yet another toy project that I named SGL (for "Standard Generic Library" since it mimics some features of C++'s STL as well as a few more utilities from the C++ standard library). Since the library is not meant to be used in real code, I decided that I would have fun and tried to implement a setjmp
/longjmp
-based exception mechanism.
First, the code. You'll have some rationale, explanation and design decisions later.
sgl/exception.h
#ifndef SGL_EXCEPTION_H_
#define SGL_EXCEPTION_H_
////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <iso646.h>
#include <setjmp.h>
#include <stdbool.h>
#include <stdnoreturn.h>
////////////////////////////////////////////////////////////
// Exceptions
/**
* @brief Basic exception type.
*
* The exceptions must be integers to satisfy the longjmp
* function. The names used here are those of the exceptions
* defined in the C++ <stdexcept> header and some other ones
* from the C++ standard library. The mechanism cannot be
* extended with user-defined types.
*
* The default sgl_exception is not meant to be thrown nor printed.
* It only exists so that it can be used to catch any exception in
* a catch block.
*/
typedef enum
{
sgl_exception = -1,
sgl_logic_error,
sgl_domain_error,
sgl_invalid_argument,
sgl_length_error,
sgl_out_of_range,
sgl_runtime_error,
sgl_range_error,
sgl_overflow_error,
sgl_underflow_error,
sgl_bad_alloc,
sgl_detail_exceptions_number
} sgl_exception_t;
#ifndef SGL_MAX_EXCEPTIONS
/**
* @def SGL_MAX_EXCEPTIONS
*
* Maximum number of nested exception blocks allowed in SGL.
* It can be set to N with the compiler option
* -DSGL_MAX_EXCEPTIONS=N
*/
#define SGL_MAX_EXCEPTIONS 32
#endif
////////////////////////////////////////////////////////////
// Global implementation variables
// Global array of jmp_buf
extern jmp_buf sgl_detail_buf_array[SGL_MAX_EXCEPTIONS];
// Whether we are in an exception catch bloc
extern bool sgl_detail_in_catch_bloc[SGL_MAX_EXCEPTIONS];
// Current exception index
extern int sgl_detail_exceptions_index;
// Current exception
extern sgl_exception_t sgl_detail_current_exception;
////////////////////////////////////////////////////////////
// Exception handling functions
/**
* Throws the given exception.
*/
noreturn void sgl_throw(sgl_exception_t exception);
/**
* Rethrows the current exception.
*/
noreturn void sgl_rethrow();
/**
* Returns whether an exception "inherits" from another
* exception so that it is possible to catch several
* related exceptions in a single catch block.
*/
bool sgl_exception_inherits_from(sgl_exception_t exception,
sgl_exception_t from);
/**
* Returns the error message associated to the given
* exception.
*/
const char* sgl_what(sgl_exception_t exception);
////////////////////////////////////////////////////////////
// Exception handling macros
/**
* @def sgl_try
*
* Beginning of an exception try bloc.
*/
#define sgl_try \
do { \
++sgl_detail_exceptions_index; \
sgl_detail_in_catch_bloc[sgl_detail_exceptions_index] = false; \
if (not setjmp(sgl_detail_buf_array[sgl_detail_exceptions_index])) \
{
/**
* @def sgl_catch(exception)
*
* Catches an exception and execute the following bloc
* of code if it corresponds to the thrown exceptions
*/
#define sgl_catch(exception) \
} \
else if (sgl_exception_inherits_from(sgl_detail_current_exception, exception)) \
{ \
sgl_detail_in_catch_bloc[sgl_detail_exceptions_index] = true; \
--sgl_detail_exceptions_index;
/**
* @def sgl_endtry
*
* If something was thrown but nothing caught, throws it again.
* Prints the current exception and aborts with -1 if we are not
* into a catch block.
*/
#define sgl_endtry \
} \
else \
{ \
if (sgl_detail_exceptions_index > 0) \
{ \
--sgl_detail_exceptions_index; \
sgl_rethrow(); \
} \
else \
{ \
sgl_terminate(); \
} \
} \
--sgl_detail_exceptions_index; \
} while (0);
////////////////////////////////////////////////////////////
// Program termination utilities
/**
* Type of the function called by sgl_terminate. While it does
* not appear in the typedef, termination handlers should be
* noreturn functions.
*/
typedef void (*sgl_terminate_handler)();
/**
* @return the current termination handler.
*/
sgl_terminate_handler sgl_get_terminate();
/**
* Sets a new termination handler. If \a new_handler is NULL,
* this functions sets back the default termination handler.
*
* @param new_handler New termination handler.
* @return the old termination handler.
*/
sgl_terminate_handler sgl_set_terminate(sgl_terminate_handler new_handler);
/**
* Calls the current termination handler. The default one
* calls abort. This function is called when an exception
* is thrown and not caught.
*/
noreturn void sgl_terminate();
#endif // SGL_EXCEPTION_H_
exception.c
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sgl/exception.h>
////////////////////////////////////////////////////////////
// Global implementation variables
jmp_buf sgl_detail_buf_array[SGL_MAX_EXCEPTIONS];
bool sgl_detail_in_catch_bloc[SGL_MAX_EXCEPTIONS];
int sgl_detail_exceptions_index = -1;
sgl_exception_t sgl_detail_current_exception;
////////////////////////////////////////////////////////////
// Throwing functions
void sgl_throw(sgl_exception_t exception)
{
assert(exception != sgl_exception);
sgl_detail_current_exception = exception;
// Handle exceptions out of a try block
if (sgl_detail_exceptions_index == -1)
{
sgl_terminate();
}
if (sgl_detail_in_catch_bloc[sgl_detail_exceptions_index])
{
--sgl_detail_exceptions_index;
}
longjmp(sgl_detail_buf_array[sgl_detail_exceptions_index], true);
}
noreturn void sgl_rethrow()
{
sgl_throw(sgl_detail_current_exception);
}
////////////////////////////////////////////////////////////
// Exceptions inheritance emulation
bool sgl_exception_inherits_from(sgl_exception_t exception,
sgl_exception_t from)
{
static uint_fast16_t inheritance[] = {
1 << sgl_logic_error,
1 << sgl_domain_error | 1 << sgl_logic_error,
1 << sgl_invalid_argument | 1 << sgl_logic_error,
1 << sgl_length_error | 1 << sgl_logic_error,
1 << sgl_out_of_range | 1 << sgl_logic_error,
1 << sgl_runtime_error,
1 << sgl_range_error | 1 << sgl_runtime_error,
1 << sgl_overflow_error | 1 << sgl_runtime_error,
1 << sgl_underflow_error | 1 << sgl_runtime_error,
1 << sgl_bad_alloc
};
if (from == sgl_exception)
{
// Everything inherits from exception
return true;
}
return inheritance[exception] & (1 << from);
}
////////////////////////////////////////////////////////////
// Exception message function
const char* sgl_what(sgl_exception_t exception)
{
static const char* const messages[] = {
"logic error",
"domain error",
"invalid argument",
"length error",
"out of range error",
"runtime error",
"range error",
"overflow error",
"underflow error",
"bad allocation"
};
if (exception < 0 || exception >= sgl_detail_exceptions_number)
{
return "unknown error";
}
return messages[exception];
}
////////////////////////////////////////////////////////////
// Program termination utilities
// Default termination function
static noreturn void default_terminate()
{
printf("Terminate called after throwing an exception.\n");
printf(" what(): %s\n", sgl_what(sgl_detail_current_exception));
abort();
}
// Current termination function
static sgl_terminate_handler current_terminate = default_terminate;
sgl_terminate_handler sgl_get_terminate()
{
return current_terminate;
}
sgl_terminate_handler sgl_set_terminate(sgl_terminate_handler new_handler)
{
sgl_terminate_handler old_handler = sgl_get_terminate();
if (new_handler == NULL)
{
new_handler = default_terminate;
}
current_terminate = new_handler;
return old_handler;
}
noreturn void sgl_terminate()
{
current_terminate();
}
Design decisions
This [part of the] library is meant to mimic C++'s exception framework. While it cannot do as many things (for example, C does not have the concept of destructors, so complex stack variables cannot be cleaned when an exception is thrown), it tries to be as close as possible, while leaving as few holes as possible in the interface. It provides features to:
- Throw an exception.
- Catch an exception.
- Rethrow the current exception.
- Display an error message for the given exception.
- Mimic a polymorphic behaviour to catch several kinds of exceptions at once.
- Allow users to specify a custom termination function for when an exception is not caught.
- Nest
try
-catch
blocks in othertry
-catch
blocks, be it in thetry
or in thecatch
.
However, that's still not C++ but mere tricks with a few macro wrappers. The kind of exceptions is fixed at compile time and compiling the library will notably produce a compiler warning since function pointers cannot be noreturn
in C11.
So, do you see anything that could be improved one way or another in this code? :)
Note: enforcing "keyword" headers such as <iso646.h>
and <stdnoreturn.h>
is a design choice. This project not being serious, it does not try to care about such things and favors eye sugar.
And since it's always better with an example to demonstrate how things work, here is one:
sgl_try
{
sgl_try
{
sgl_throw(sgl_out_of_range);
}
sgl_catch (sgl_logic_error)
{
printf("\nCaught exception: %s\n", sgl_what(sgl_logic_error));
sgl_rethrow();
}
sgl_endtry
}
sgl_catch (sgl_out_of_range)
{
printf("\nCaught exception: %s\n", sgl_what(sgl_out_of_range));
sgl_try
{
sgl_throw(sgl_bad_alloc);
}
sgl_catch(sgl_domain_error)
{
printf("\nCaught exception: %s\n", sgl_what(sgl_domain_error));
}
sgl_catch(sgl_exception)
{
printf("\nCaught exception: %s\n", sgl_what(sgl_exception));
}
sgl_endtry
}
sgl_endtry
This program should have the following output:
Caught exception: logic error Caught exception: out of range error Caught exception: unknown error
finally
(from GC'd languages like Java/C#) to solve the issue of cleaning up without RAII-style destructors. \$\endgroup\$register_for_cleanup
function at creation, but it may indeed be easier to create somefinally
mechanism. I will see what I can do about it :) \$\endgroup\$_t
as a suffix for your types. Those names are reserved in POSIX. \$\endgroup\$toggle
also violates the C standard and nobody will ever bother you for it. Technically speaking, any name could be claimed by the standard at some point if you're not lucky enough, reserved or not. While reserving names for the implementation is ok, reserving them for a potential future standardization is rather hazardous IMO since they're gonna standardize names that they never reserved anyway. \$\endgroup\$