7
\$\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\$
1
  • \$\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, 2016 at 21:06

1 Answer 1

2
\$\begingroup\$

It's very heavy on macros, but that's inevitable when we provide functions to extend the language in this way. Users don't have to look at the full gory details anyway, and the user interface looks good. I like the consistent prefix, which makes it very easy to see at a glance that they are related.

It would be instructive to show a function that accepts a result type, and acts on it (if valid) or propagates the error (otherwise), in the same way that NaN values propagate through arithmetic. There might even be a case for another macro, perhaps like

#define RESULT_RETURN_IF_ERR(type, val, result_type)    \
    do {                                                \
        if (RESULT_IS_ERR(type)(val))                   \
            return RESULT_ERR(result_type)(val.err);    \
    } while (0)

The panic() function could probably be replaced at call sites by panicf("%s", message). It might be an idea to add newline to the printing (especially as the messages used in the code are not full lines in themselves).

Perhaps also consider printing the type via %s formatting, to slightly reduce code bloat (so all the expansions of RESULT_UNWRAP() and RESULT_UNWRAP_ERR share the same pair of format strings). The slight performance penalty is trivial, and in a path that shouldn't be exercised in a well-written program.

#define RESULT_DEFINE(type) \
    ⋮
    type RESULT_UNWRAP(type)(const RESULT(type) * res) {                \
        if (RESULT_IS_ERR(type)(res))                                   \
            panicf("Attempted to unwrap empty Result of type "          \
                   "%s. Instead had error: %s", #type, res->err);       \
        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: %s", #type);          \
        return res->err;                                                \
    }
\$\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.