8
\$\begingroup\$

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 other try-catch blocks, be it in the try or in the catch.

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
\$\endgroup\$
4
  • \$\begingroup\$ Very creative implementation! You might consider attempting to implement something akin to finally (from GC'd languages like Java/C#) to solve the issue of cleaning up without RAII-style destructors. \$\endgroup\$
    – glampert
    Jun 30 '15 at 18:16
  • \$\begingroup\$ @glampert Good idea. I was wondering how to emulate RAII with classes self-registering to some register_for_cleanup function at creation, but it may indeed be easier to create some finally mechanism. I will see what I can do about it :) \$\endgroup\$
    – Morwenn
    Jul 1 '15 at 8:01
  • \$\begingroup\$ In this case it's not really an issue, but it's bad practice to use _t as a suffix for your types. Those names are reserved in POSIX. \$\endgroup\$
    – jacwah
    Jul 1 '15 at 10:03
  • \$\begingroup\$ @jacwah Yeah, that's a convention that I always knowingly violate. On the other hand, naming a function 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\$
    – Morwenn
    Jul 1 '15 at 10:16
4
\$\begingroup\$

Purpose of sgl_detail_in_catch_bloc[]?

It looks like the only use of sgl_detail_in_catch_bloc[] is in sgl_throw():

if (sgl_detail_in_catch_bloc[sgl_detail_exceptions_index])
{
    --sgl_detail_exceptions_index;
}

But that if statement should never be true because the only place that sets an array entry to true immediately decrements the index as well:

    sgl_detail_in_catch_bloc[sgl_detail_exceptions_index] = true;
    --sgl_detail_exceptions_index;

And the only place that can increment the index sets the array entry to false. In other words, I don't think that sgl_detail_in_catch_bloc[] currently serves any purpose, and all uses of it could be deleted without altering the behavior of your code.

\$\endgroup\$
1
  • \$\begingroup\$ It seems that the latest bugfix that I introduced in the code removed the need for sgl_detail_in_catch_bloc altogether, but I never noticed it. Thanks for pointing it to me :) \$\endgroup\$
    – Morwenn
    Jul 1 '15 at 7:57
7
\$\begingroup\$

Some small stuff:

sgl_detail_current_exception is initialize to 0 as it is in global memory. Given the enumerated list starts with -1, then sgl_logic_error == 0:

sgl_exception_t sgl_detail_current_exception;
// is effectively 
sgl_exception_t sgl_detail_current_exception = sgl_logic_error;

Recommend adding an enumerated value of sgl_none = 0 to distinguish the initial state and explicitly initialize sgl_detail_current_exception.

typedef enum
{
    sgl_exception = -1,
    sgl_none = 0,
    sgl_logic_error,
    sgl_domain_error,
    ...

sgl_exception_t sgl_detail_current_exception = sgl_none;

Rather than messages[], use messages[sgl_detail_exceptions_number] as that array size must be a matching value. This will help should only one of messages[] or sgl_exception_t errantly change. Similar for inheritance[].


Potential inconsistencies with sgl_detail_exceptions_number not limited to 15, uint_fast16_t, int range may be as small as -32767 to 32767. Suggest:

assert(sgl_detail_exceptions_number < 16);

// use const
static const uint_fast16_t inheritance[] = {
    1u << sgl_logic_error,   // add 'u' here and many other places
    ...

Did not notice any limit in code to incrementing sgl_detail_exceptions_index. suggest adding test to insure sgl_detail_exceptions_index < SGL_MAX_EXCEPTIONS before access arrays.

#define sgl_try                                                             \
    do {                                                                    \
        ++sgl_detail_exceptions_index;                                      \
        //  Hmmm: out of bounds potential
        sgl_detail_in_catch_bloc[sgl_detail_exceptions_index] = false;      \
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.