239 lines
10 KiB
Markdown
239 lines
10 KiB
Markdown
|
|
# Using the library
|
||
|
|
|
||
|
|
## (Optional) Configuring the logging function
|
||
|
|
|
||
|
|
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
||
|
|
|
||
|
|
```
|
||
|
|
void my_logger(const char *fmt, ...)
|
||
|
|
{
|
||
|
|
/* ... do something */
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
/* set your custom error handler */
|
||
|
|
akerr_log_method = &my_logger;
|
||
|
|
|
||
|
|
/* proceed to use the library */
|
||
|
|
```
|
||
|
|
|
||
|
|
## Setting Up the Error Context
|
||
|
|
|
||
|
|
Before you can use any of these macros you must set up an error context inside of the current scope.
|
||
|
|
|
||
|
|
```c
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
```
|
||
|
|
|
||
|
|
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
||
|
|
|
||
|
|
## Attempting an Operation
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
// ... code
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true)
|
||
|
|
```
|
||
|
|
|
||
|
|
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
||
|
|
|
||
|
|
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
||
|
|
|
||
|
|
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
||
|
|
|
||
|
|
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORETURN(errctx)` instead.
|
||
|
|
|
||
|
|
|
||
|
|
## Capturing errors
|
||
|
|
|
||
|
|
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
||
|
|
|
||
|
|
### Capturing errors from functions which return akerr_ErrorContext *
|
||
|
|
|
||
|
|
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, errorGeneratingFunction())
|
||
|
|
} // ...
|
||
|
|
```
|
||
|
|
|
||
|
|
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||
|
|
|
||
|
|
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
||
|
|
|
||
|
|
### Setting errors from functions or expressions returning integer
|
||
|
|
|
||
|
|
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
||
|
|
|
||
|
|
Here is an example of checking for a NULL pointer
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
FAIL_ZERO_BREAK(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||
|
|
} // ...
|
||
|
|
```
|
||
|
|
|
||
|
|
Here is an example of checking for two strings that are not equal
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||
|
|
} // ...
|
||
|
|
```
|
||
|
|
|
||
|
|
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||
|
|
|
||
|
|
### Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
||
|
|
|
||
|
|
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
for ( int i = 0; i < n; i++ ) {
|
||
|
|
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
||
|
|
}
|
||
|
|
// ... this code still executes, with errctx already in an error state ...
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
```
|
||
|
|
|
||
|
|
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
||
|
|
|
||
|
|
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
||
|
|
|
||
|
|
```c
|
||
|
|
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
for ( int i = 0; i < n; i++ ) {
|
||
|
|
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
||
|
|
}
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
||
|
|
|
||
|
|
```c
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
||
|
|
} CLEANUP {
|
||
|
|
// ... always runs ...
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE(errctx, AKERR_VALUE) {
|
||
|
|
// ... handle a failure from any iteration ...
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Passing errors
|
||
|
|
|
||
|
|
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
||
|
|
|
||
|
|
```
|
||
|
|
PREPARE_ERROR(e);
|
||
|
|
PASS(e, some_method_that_may_fail());
|
||
|
|
SUCCEED_RETURN(e);
|
||
|
|
```
|
||
|
|
|
||
|
|
This does the same thing as this, but with less code:
|
||
|
|
|
||
|
|
```
|
||
|
|
PREPARE_ERROR(e);
|
||
|
|
ATTEMPT {
|
||
|
|
CATCH(e, some_method_that_may_fail());
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(e) {
|
||
|
|
} FINISH(e, true);
|
||
|
|
SUCCEED_RETURN(e);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Handling errors
|
||
|
|
|
||
|
|
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
||
|
|
|
||
|
|
### Handling a specific error with HANDLE
|
||
|
|
|
||
|
|
In order to handle a specific error code, use the `HANDLE` macro.
|
||
|
|
|
||
|
|
```c
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||
|
|
// Something is complaining about a null pointer error. Do something about it.
|
||
|
|
} // ...
|
||
|
|
```
|
||
|
|
|
||
|
|
### Handling a group of errors with HANDLE_GROUP
|
||
|
|
|
||
|
|
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
||
|
|
|
||
|
|
```c
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE(errctx, AKERR_IO) {
|
||
|
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||
|
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||
|
|
// error handling code goes here
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
||
|
|
|
||
|
|
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
||
|
|
|
||
|
|
```c
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} HANDLE(errctx, AKERR_IO) {
|
||
|
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||
|
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||
|
|
// This code handles 3 error cases
|
||
|
|
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||
|
|
// This code handles 1 error case
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Returning success or failure from functions returning akerr_ErrorContext *
|
||
|
|
|
||
|
|
If at all possible, when using this library, your functions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
||
|
|
|
||
|
|
### SUCCEED_RETURN
|
||
|
|
|
||
|
|
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
||
|
|
|
||
|
|
```c
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
ATTEMPT {
|
||
|
|
// ... stuff
|
||
|
|
} CLEANUP {
|
||
|
|
} PROCESS(errctx) {
|
||
|
|
} FINISH(errctx, true);
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
```
|
||
|
|
|
||
|
|
### FAIL_RETURN
|
||
|
|
|
||
|
|
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
||
|
|
|
||
|
|
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
||
|
|
|
||
|
|
```c
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
||
|
|
```
|
||
|
|
|
||
|
|
### Conditionally failing and returning
|
||
|
|
|
||
|
|
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
||
|
|
|
||
|
|
```c
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_ZERO_RETURN(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||
|
|
```
|
||
|
|
|
||
|
|
```c
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||
|
|
```
|