exceptions4c 4.0
Exceptions for C
Loading...
Searching...
No Matches
exceptions4c

Introduction

Bring the power of exceptions to your C applications!

Note
This library provides you with a set of macros and functions that map the exception handling semantics you are probably already used to.

Getting Started

Adding Exceptions to Your Project

This library consists of two files:

To use it in your project, include the header file in your source code files.

#include <exceptions4c.h>
An exception handling library for C.

And then link your program against the library code.

Remarks
There is also a lightweight version of this library, intended for small projects and embedded systems. exceptions4c-lite is a header-only library that provides the core functionality of exceptions4c in just one file.

Defining Exception Types

Create meaningful exceptions that reflect problematic situations in the program.

/* Generic errors */
const struct e4c_exception_type NOT_ENOUGH_MEMORY = {NULL, "Not enough memory"};
/* Base exception for all pet-related errors */
const struct e4c_exception_type PET_ERROR = {NULL, "Pet error"};
/* Specific types of pet errors */
const struct e4c_exception_type PET_NOT_FOUND = {&PET_ERROR, "Pet not found"};
const struct e4c_exception_type PET_STORE_CLOSED = {&PET_ERROR, "Pet store closed"};

An exception type is a simple structure with an optional supertype and a default error message.

Note
Exception types create a hierarchy, where a more specific type can be built upon a more generic one.

Basic Usage

Exception handling lets a program deal with errors without crashing. When something goes wrong, the program pauses its normal flow, jumps to code that handles the issue, and then either recovers or exits cleanly.

This library provides the following macros that are used to handle exceptions:

Throwing Exceptions

When we THROW an exception, the flow of the program moves to the appropriate CATCH block. If the exception is not handled by any of the blocks in the current function, it propagates up the call stack to the function that called the current function. This continues until the top level of the program is reached. If no block handles the exception, the program terminates and an error message is printed to the console.

Use THROW to trigger an exception when something goes wrong.

/* Returns a pet by id */
Pet pet_find(int id) {
Pet pet = pet_clone(id);
if (!pet) {
THROW(PET_NOT_FOUND, "Pet %d not found", id);
}
return pet;
}

When we THROW an exception, the flow of the program moves from the TRY block to the appropriate CATCH block. If the exception is not handled by any of the blocks in the current function, it propagates up the call stack to the function that called the current function. This continues until the top level of the program is reached. If no block handles the exception, the program terminates and an error message is printed to the console.

Note
Error messages can be formatted, just as you would with printf. Additionally, if you don't provide an error message, the default one for that exception type will be used.

Trying Risky Code

Use a TRY block to wrap code that might cause an exception.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
TRY {
status = pet_find(id)->status;
}
return status;
}

These code blocks, by themselves, don't do anything special. But they allow the introduction of other blocks that do serve specific purposes.

Remarks
A single TRY block must be followed by one or more CATCH blocks to handle the errors, and an optional FINALLY block to execute cleanup code.
Attention
Never exit these blocks using goto, break, continue, or return.

Catching Exceptions

To prevent the program from crashing, exceptions need to be handled properly in designated sections of the code.

Handling Specific Types of Exceptions

Use a CATCH block to handle a specific type of exceptions when they occur.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
TRY {
status = pet_find(id)->status;
} CATCH (PET_ERROR) {
status = UNKNOWN;
} CATCH (NOT_ENOUGH_MEMORY) {
abort();
}
return status;
}

If the type in the CATCH block is the same as (or a supertype of) the thrown exception, then the block will be used to handle it.

One or more CATCH blocks can follow a TRY block. Each CATCH block must specify the type of exception it handles. If its type doesn't match the thrown exception, then that block is ignored, and the exception may be caught by the following blocks.

Important
When looking for a match, CATCH blocks are inspected in the order they appear. If you place a generic handler before a more specific one, the second block will be unreachable.

Handling All Kinds of Exceptions

On the other hand, the CATCH_ALL block is a special block that can handle all types of exceptions.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
TRY {
status = pet_find(id)->status;
if (e4c_get_exception()->type == &NOT_ENOUGH_MEMORY) {
abort();
}
status = UNKNOWN;
}
return status;
}

Only one CATCH_ALL block is allowed per TRY block, and it must appear after all type-specific CATCH blocks if any are present.

Remarks
Use e4c_get_exception to retrieve the exception currently being handled.

Ensuring Cleanup

A FINALLY block always runs, no matter whether an exception happens or not.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
Pet pet = NULL;
TRY {
pet = pet_find(id);
status = pet->status;
} CATCH (PET_NOT_FOUND) {
status = UNKNOWN;
} FINALLY {
pet_free(pet);
}
return status;
}

This block is optional. And, for each TRY block, there can be only one FINALLY block. If an exception occurs, the FINALLY block is executed after the CATCH or block that can handle it. Otherwise, it is executed after the TRY block.

Remarks
Use e4c_is_uncaught to determine whether the thrown exception hasn't been handled yet.

Advanced Usage

Dispose Pattern

This is a powerful design pattern for resource management. It is a clean and terse way to handle the acquisition and disposal of all kinds of resources.

These macros will help you make sure that no resource is leaked in your program.

Simple Resource Acquisition

A USING block allows you to easily acquire and dispose of a resource. It is similar to a for statement, because it receives three comma-separated expressions that will be evaluated in order.

  • An acquisition expression that will try to acquire the resource.
  • A test expression that defines the condition for using the resource.
  • A disposal expression that will dispose of the resource.

Both these expressions and the code block that uses the resource are free to throw exceptions.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
Pet pet = NULL;
USING (pet = pet_find(id), pet != NULL, pet_free(pet)) {
status = pet->status;
}
return status;
}
  1. The resource will be acquired, using the expression pet = pet_find(id).
  2. If the expression pet != NULL holds true, then the USING block will be executed.
  3. The resource pet will be disposed of, using the expression pet_free(pet), no matter whether an exception happens or not.

You can append CATCH blocks to deal with exceptions that may happen during the manipulation of the resource. Just remember: by the time the CATCH block is executed, the resource will already have been disposed of.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
Pet pet = NULL;
USING (pet = pet_find(id), pet != NULL, pet_free(pet)) {
status = pet->status;
} CATCH (PET_NOT_FOUND) {
status = UNKNOWN;
}
return status;
}
Remarks
You can even append a FINALLY block for cleanup code other than disposing of the resource.

Complex Resource Acquisition

Use a WITH block when the steps to acquire a resource are more complex than simply evaluating an expression. It works exactly the same as the USING block, except that you can write the code block in charge of actually acquiring the resource.

/* Returns the status of a pet by id */
pet_status get_pet_status(int id) {
pet_status status = ERROR;
Pet pet = NULL;
WITH (pet_free(pet)) {
if (pet_store_is_closed()) {
THROW(PET_STORE_CLOSED, NULL);
}
pet = pet_find(id);
} USE (pet != NULL) {
status = pet->status;
} CATCH (PET_NOT_FOUND) {
status = UNKNOWN;
}
return status;
}
Remarks
You can also append CATCH blocks and an optional FINALLY block.

Customization

To customize the way this library behaves you may configure a structure that represents the exception context of the program.

Retrieving the Exception Context

Use e4c_get_context to retrieve the current exception context of the program.

struct e4c_context * context = e4c_get_context();

Then use this object to set up different handlers.

Custom Exception Initializer

Exceptions support custom data. By default, this data is left uninitialized when an exception is thrown.

You can set a custom exception initializer and your function will be executed whenever an exception is thrown.

const struct e4c_exception_type PET_ERROR = {NULL, "Pet error"};
static void set_custom_data(struct e4c_exception * exception) {
exception->data = "My custom data";
}
int main(void) {
e4c_get_context()->initialize_exception = set_custom_data;
TRY {
THROW(PET_ERROR, "Bad dog");
const char * data = e4c_get_exception()->data;
printf("Custom data: %s\n", data);
}
return EXIT_SUCCESS;
}
Remarks
For example, you could use this opportunity to capture the entire stacktrace of your program.

Custom Exception Finalizer

You can also set a exception finalizer to execute your function whenever an exception is deleted.

struct my_custom_data { int id; const char * msg; };
static void my_initializer(struct e4c_exception * exception) {
struct my_custom_data data = {123, "Hello world!"}, * tmp;
if ((tmp = exception->data = malloc(sizeof(data)))) *tmp = data;
}
static void my_finalizer(const struct e4c_exception * exception) {
free(exception->data);
}
int main(void) {
struct e4c_context * context = e4c_get_context();
context->initialize_exception = my_initializer;
context->finalize_exception = my_finalizer;
TRY {
THROW(PET_ERROR, "Bad dog");
const struct my_custom_data * data = e4c_get_exception()->data;
printf("ID: %d MSG: %s\n", data->id, data->msg);
}
return EXIT_SUCCESS;
}
Remarks
This allows you to free any resources you acquired when you initialized an exception's custom data.

Custom Uncaught Handler

By default, when an exception reaches the top level of the program, it gets printed to the standard error stream.

You can customize this behavior by setting the uncaught handler to a custom function that will be executed in the event of an uncaught exception.

static void my_uncaught_handler(const struct e4c_exception * exception) {
fprintf(stderr, "UNCAUGHT: %s\n", exception->message);
}
int main(int argc, char * argv[]) {
e4c_get_context()->uncaught_handler = my_uncaught_handler;
THROW(MY_ERROR, "Oops");
}
Remarks
Instead of simply using stderr you could save an error report in a local file.

Custom Termination Handler

After the uncaught handler has been executed, the program is terminated by calling exit(EXIT_FAILURE).

You can make the library do anything else by setting the termination handler to execute a function in the event of program termination.

static void my_termination_handler(void) {
exit(EXIT_SUCCESS);
}
int main(void) {
e4c_get_context()->termination_handler = &my_termination_handler;
THROW(PET_ERROR, "Bad dog");
}
Remarks
In a multithreaded program, you may want to cancel the current thread, instead of terminating the whole program.

Exception Context Supplier

By default, a predefined exception context is provided and used by the library. But you can create a supplying function and pass it to e4c_set_context_supplier so you are in full control of your program's exception context.

static struct e4c_context my_custom_context = {
.initialize_exception = my_initializer,
.finalize_exception = my_finalizer
};
static struct e4c_context * my_context_supplier(void) {
return &my_custom_context;
}
int main(void) {
e4c_set_context_supplier(my_context_supplier);
TRY {
THROW(PET_ERROR, "Bad dog");
const struct my_custom_data * data = e4c_get_exception()->data;
printf("MSG: %s\n", data->msg);
}
return EXIT_SUCCESS;
}
Remarks
This mechanism can be useful to provide a concurrent exception handler. For example, your custom context supplier could return different instances, depending on which thread is active.

Multithreading

There is an extension for this library, intended for multithreaded programs. exceptions4c-pthreads allows you to safely and concurrently use exceptions.

All you have to do is set the exception context supplier, so that each POSIX thread gets its own exception context.

const struct e4c_exception_type OOPS = {NULL, "Oops"};
/* A Thread that throws an exception */
static void *my_thread(void *arg) {
THROW(OOPS, "Oh no");
}
int main(void) {
/* Set the thread-safe exception context supplier */
e4c_set_context_supplier(&e4c_pthreads_context_supplier);
/* Start the thread */
pthread_t thread;
pthread_create(&thread, NULL, my_thread, NULL);
pthread_join(thread, NULL);
/* The program was not terminated, only the thread was canceled */
return EXIT_SUCCESS;
}

In the event of an uncaught exception, instead of terminating the program, only the current thread will be canceled.

Note
Read the docs for more information about this extension.

Signal Handling

You can turn some standard signals such as SIGTERM, SIGFPE, and SIGSEGV into exceptions so they can be handled in a regular CATCH block. For example, you could do that to prevent your program from crashing when a null pointer is dereferenced.

#include <stdio.h>
#include <signal.h>
#include <exceptions4c.h>
const struct e4c_exception_type SEGFAULT = {NULL, "Segmentation fault"};
void segfault(int sigsegv) {
signal(sigsegv, segfault);
THROW(SEGFAULT, NULL);
}
int main(void) {
const int * null_pointer = NULL;
signal(SIGSEGV, segfault);
TRY {
printf("Oh no %d", *null_pointer);
} CATCH (SEGFAULT) {
printf("Danger avoided!\n");
}
return EXIT_SUCCESS;
}

However, it's easy to enter undefined behavior territory, due to underspecified behavior and significant implementation variations regarding signal delivery while a signal handler is executed, so use this technique with caution.

Important
Keep in mind that the behavior is undefined when signal is used in a multithreaded program.

Additional Info

Compatibility

This library relies on modern C features such as designated initializers, compound literals, and __VA_OPT__.

Remarks
If you need support for older compilers, you can try exceptions4c-lite. It's header-only and fully compatible with ANSI C. And if you're looking for a cleaner, safer, and more modern approach to error handling that doesn't involve throwing or catching exceptions, you may want to take a look at Result Library.

Caveat

Exception handling is based on standard C library functions setjmp to save the current execution context and longjmp to restore it. According to the documentation:

Upon return to the scope of setjmp:

  • all accessible objects, floating-point status flags, and other components of the abstract machine have the same values as they had when longjmp was executed,
  • except for the non-volatile local variables in the function containing the invocation of setjmp, whose values are indeterminate if they have been changed since the setjmp invocation.

Since each TRY block invokes setjmp, modified local variables in scope must be volatile.

Similar Projects

There are other exception handling implementations and libraries.

Here you can find some of them, in no particular order.

Releases

This library adheres to Semantic Versioning. All notable changes for each version are documented in a change log.

Head over to GitHub for the latest release.

Latest Release

Source Code

The source code is available on GitHub.

Fork me on GitHub