3
\$\begingroup\$

There are frequently things I miss when using C; one of them is a nice error-handling (exception-like) system. After trying Rust, I realized I could implement something similar in C itself. So here's my attempt at writing a Result type in C.

The idea of the Result type is similar to that of an Optional type: if the value was able to be computed, then we return that. Otherwise, if there was an error, we return some error. In my case, my errors are string messages.

I plan on using this for a class where we are required to use C, not C++ or other systems languages. We will be using gcc, so gcc specific behaviour is perfectly okay.

panic.h

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

/**
 * @brief panics with the error message, terminating the program
 * @details This function never returns. It writes `str` to `stderr` and exits
 *          with an error code of `1`.
 * 
 * @param str The error message displayed on termination
 */
void panic(const char * str) __attribute__((noreturn));

void panic(const char * str) {
    fputs(str, stderr);
    exit(1);
}

/**
 * @brief panics with the error message, terminating the program
 * @details This function never returns. It writes to `stderr` and exits
 *          with an error code of `1`. This function calls `printf` to
 *          allow message formatting
 * 
 * @param str The error message displayed on termination
 */
void panicf(const char * fmt, ...) __attribute__((noreturn));

void panicf(const char * fmt, ...) {
    va_list args;
    va_start(args, fmt);

    vfprintf(stderr, fmt, args);

    exit(1);
}

result.h

#include <stdbool.h>
#include <stdint.h>

#include "panic.h"

/**
 * @file result.h
 * Provides an alternative to exceptions, whose semantics are slightly better than exceptions
 * 
 * C does not support exceptions. Many C functions instead return a status code
 * reporting what happened. Alternatively, many C libraries allow a callback to
 * be logged in order to report a detailed description of any errors that 
 * occurred. However, the programming language known as Rust deliberately left
 * out exceptions; instead they opted for return values to report errors, with a
 * `RESULT` type. This type contains the actual value if there is one, or some
 * error otherwise.
 * 
 * This is the approach that this file defines.
 */

/**
 * @brief A macro which gives the value of a `RESULT` if present, else returns
 *        the result.
 * @details Do not use this twice on the same line.
 *          
 *          Example Usage:
 *          @code
 *          RESULT(int) someResultFromFn = ...;
 *          
 *          int value;
 *          
 *          RESULT_M_TRY(int, value, someResultFromFn);
 *          @endcode
 * 
 * @param type The type of the `RESULT(type)`
 * @param dest The location to write the new result into
 * @param result The old result
 * @return nothing. (wrapped in `do-while`)
 * 
 * @see RESULT_M_TRY_DECL
 */
#define RESULT_M_TRY(type, dest, result)                                  \
    do {                                                                \
        RESULT(type) _result_ ## type ## _try_at_ ## __LINE__ = result; \
        if (_result_ ## type ## _try_at_ ## __LINE__ .err) {            \
            return _result_ ## type ## _try_at_ ## __LINE__;            \
        }                                                               \
        dest = _result_ ## type ## _try_at_ ## __LINE__ .value;         \
    } while (0)

/**
 * @brief A macro which gives the value of a `RESULT` if present, else returns
 *        the result.
 * @details Do not use this twice on the same line.
 * 
 *          Example Usage:
 *          @code
 *          RESULT(int) myFn(int someParam) {
 *              ....
 *          }
 *          
 *          RESULT(int) function() {
 *              RESULT_M_TRY_DECL(int, val, myFn(1));
 *              // equivalent to:
 *              RESULT(int) r = myFn(1);
 *              if (!RESULT_IS_OK(int)(r)) return r;
 *              int val = RESULT_UNWRAP(r);
 *          }
 *          @endcode
 * 
 * @param type The type of the `RESULT(type)`
 * @param dest The location to write the new result into
 * @param result The old result
 * @return nothing. (wrapped in `do-while`)
 * 
 * @see RESULT_M_TRY
 */
#define RESULT_M_TRY_DECL(type, dest, result) \
    type dest; \
    RESULT_M_TRY(type, dest, result)

/**
 * @brief Template for a RESULT class (similar to the one that the language Rust
 *        has). This is the typename.
 * @details This macro expands to a unique name per type, so `RESULT(int)` is
 *          roughly equivalent to the C++ `RESULT<int>`. Note that
 *          `RESULT(int*)` will not compile; if you want to use something along
 *          those lines, you must typedef it first.
 * 
 *          For the error strings, you must use string literals or something with
 *          storage duration, so the pointer doesn't become invalidated.
 * 
 * @param  The template parameter
 * @return `Result_ type`, so `RESULT(int)` is `Result_int` Each macro of this sort
 *         follows that convention, and you can rely on it
 */
#define RESULT(type) Result_ ## type

// Class functions
/**
 * @brief Creates a result which holds a value of `type`
 * @details Indicates that no error occurred, and that the computed value from
 *          the function can be retrieved via `RESULT_UNWRAP`.
 * 
 * @param type value
 * @return Returns a `RESULT(type)` containing a `type`
 */
#define RESULT_OK(type) Result_ ## type ## _Ok

/**
 * @brief Creates a result which indicates that an error occurred.
 * @details Indicates that an error occurred in the function. Similar to an
 *          exception in other languages.
 * 
 * @param const char * error message
 * @return Returns a `RESULT(type)` that does not contain a `type`
 */
#define RESULT_ERR(type) Result_ ## type ## _Err

// methods
/**
 * @brief Tests if the `RESULT` actually contains a `type` (else it was an error)
 * 
 * @return `true` if calling `RESULT_UNWRAP` is safe, else `false`
 */
#define RESULT_IS_OK(type)     Result_ ## type ## _is_ok

/**
 * @brief Tests if the `RESULT` doesn't contain a `type` (else it was ok)
 * 
 * @return `true` if calling `RESULT_UNWRAP_ERR` is safe, else `false`
 */
#define RESULT_IS_ERR(type)    Result_ ## type ## _is_err

/**
 * @brief Unwraps the `RESULT` into a `type`, panicking if impossible
 * @details If `RESULT_IS_OK`, this returns the `type` value of the `RESULT`.
 *          Otherwise, this panics.
 * 
 * @return `type` that is contained in the `RESULT`, or could `exit()`
 * 
 * @see RESULT_EXPECT
 */
#define RESULT_UNWRAP(type)    Result_ ## type ## _unwrap

/**
 * @brief Unwraps the `RESULT` into a `type`, panicking with the error message
 *        if impossible
 * @details Similar to `RESULT_UNWRAP`, except this has a specialized error
 *          message
 * 
 * @param const char * the error message on failure
 * @return `type` contained in `RESULT`, else terminates the program
 * 
 * @see RESULT_UNWRAP
 */
#define RESULT_EXPECT(type)    Result_ ## type ## _expect

/**
 * @brief Unwraps the `RESULT` into a `type`, returning the supplied alternative
 *        otherwise
 * @details If `RESULT_IS_OK`, this returns the `type` value of the `RESULT`.
 *          Otherwise, this returns the alternative supplied in the function
 *          arguments.
 * 
 * @param type or_else The value returned if the unwrap failed
 * @return `type` contained in `RESULT`, or the alternative
 * 
 * @see RESULT_UNWRAP
 * @see RESULT_EXPECT
 */
#define RESULT_UNWRAP_OR(type) Result_ ## type ## _unwrap_or

/**
 * @brief Unwraps the `RESULT` into an error string, panicking if impossible
 * @details If `RESULT_IS_ERR`, this returns error message of the `RESULT`.
 *          Otherwise, this panics.
 * 
 * @param const char * the error string
 * @return error string in the RESULT, or could terminate
 * 
 * @see RESULT_EXPECT
 */
#define RESULT_UNWRAP_ERR(type) Result_ ## type ## _unwrap_err

/**
 * @brief Declares a RESULT type for use.
 * @details This declares the struct and functions for a given type. Note that
 *          the functions still need to be defined. The values inside the struct
 *          are implementation details, and may change at any time; do not use
 *          them.
 * 
 * @param  template parameter
 * 
 * @see RESULT_DEFINE
 */
#define RESULT_DECLARE(type)      \
    typedef struct RESULT(type) { \
        type value;               \
        /* string literals only */\
        const char * err;         \
    } RESULT(type);               \
    RESULT(type) RESULT_OK(type)(type value);        \
    RESULT(type) RESULT_ERR(type)(const char *err);  \
    bool RESULT_IS_OK(type)(const RESULT(type) *);                \
    bool RESULT_IS_ERR(type)(const RESULT(type) *);               \
    type RESULT_UNWRAP(type)(const RESULT(type) *);               \
    type RESULT_EXPECT(type)(const RESULT(type) *, const char *); \
    type RESULT_UNWRAP_OR(type)(const RESULT(type) *, type);      \
    const char * RESULT_UNWRAP_ERR(type)(const RESULT(type) *);

/**
 * @brief Defines all the functions for the given `RESULT(type)`
 * @details Defines each and every function necessary to use the `RESULT`.
 * 
 * @param  template parameter
 * 
 * @see RESULT_DECLARE
 */
#define RESULT_DEFINE(type) \
    RESULT(type) RESULT_OK(type)(type value) { \
        RESULT(type) res = {                   \
            .value = value,                    \
            .err = NULL                        \
        };                                     \
        return res;                            \
    } \
    RESULT(type) RESULT_ERR(type)(const char *err) { \
        RESULT(type) res = {          \
            .err = err                \
        };                            \
        return res;                   \
    } \
    bool RESULT_IS_OK(type)(const RESULT(type) * res) { \
        return res->err == NULL;                        \
    } \
    bool RESULT_IS_ERR(type)(const RESULT(type) * res) { \
        return res->err != NULL;                         \
    } \
    type RESULT_UNWRAP(type)(const RESULT(type) * res) {                                 \
        if (RESULT_IS_ERR(type)(res)) panicf("Attempted to unwrap empty Result of type " \
                                             #type ". Instead had error: %s", res->err); \
        return res->value;                                                               \
    } \
    type RESULT_EXPECT(type)(const RESULT(type) * res, const char * message_on_err) { \
        if (RESULT_IS_ERR(type)(res)) panic(message_on_err);                          \
        return res->value;                                                            \
    } \
    type RESULT_UNWRAP_OR(type)(const RESULT(type) * res, type else_val) { \
        if (RESULT_IS_ERR(type)(res)) return else_val;                     \
        return res->value;                                                 \
    } \
    const char * RESULT_UNWRAP_ERR(type)(const RESULT(type) * res) {                 \
        if (RESULT_IS_OK(type)(res)) panic("Result was not an error; type: " #type); \
        return res->err;                                                             \
    }

Example Usage:

#include "result.h"

RESULT_DECLARE(uint32_t)
RESULT_DEFINE(uint32_t)

Result_uint32_t f1(uint32_t i) {
    if (i == 0) return Result_uint32_t_Err("Error");
    return Result_uint32_t_Ok(i);
}

Result_uint32_t f2(uint32_t i, uint32_t i2) {
    RESULT_M_TRY_DECL(uint32_t, val, f1(i));
    return Result_uint32_t_Ok(val + i2);
}

int main() {
    Result_uint32_t res = f2(0, 1);
    Result_uint32_t res2 = f2(1, 1);

    printf("hi %d\n", Result_uint32_t_unwrap_or(&res, 0));
    printf("hi %d\n", Result_uint32_t_unwrap_or(&res2, 0));
    fflush(stdout);
    printf("hi %s\n", Result_uint32_t_unwrap_err(&res2));
}
\$\endgroup\$
  • \$\begingroup\$ Now that I think about it, I could probably have just used void * and forgotten about macros, but then there's a casting annoyance because we use a c++ compiler instead of a c compiler.... and now I remember that I did think of this but didn't want to pay for using the heap at all. \$\endgroup\$ – Justin Sep 1 '16 at 21:06

Your Answer

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

Browse other questions tagged or ask your own question.