0

This is more of an aesthetic question than a purely functional one, but I want to know if there's any answer. See, I'm programming an error reporter for a project I've recently begun working on, and it's mostly code-based for ease of function use. Think of errno, with various values defined to note specific problems. It's a prevalent system among error handlers. However, I don't want to just give the user a code, because I know from the time before I started programming that a random array of numbers can often be overwhelming when trying to self-diagnose the problem.

I want to give them a string representation of the code. However, I cannot think of a pragmatic way to do this. A stringify (#) macro wouldn't work because the error code sent to the function is unknown at compile time. Is there something beyond a large switch that could rectify this issue?

For context, this is my little error logger; this is the last step of the error pipeline, if the program gets here, a fatal error has occurred.

// Notes: We don't do a lot of error-checking in this function, beyond
// the stuff built into PushNotification. The reason for that is
// fairly simple; at this point, we don't give a darn. If the program
// gets here, something's irreversibly wrong regardless. 
// Last edit: July 3rd, 2024
_Noreturn void InternalLogError(const char* caller, const char* file, u32 line,
                      u32 code)
{

    char error_message[128];
    // This is the place in which I want to add the code-to-string bit.
    snprintf(error_message, 128,
             "\nMemphim failed catastrophically.\n"
             "Code: %d.\nCaller: %s @ %s\nLine: %d\n",
             code, caller, file, line);

    if (CheckShellAvailability()) puts(error_message);
    else PushNotification("Memphim Error Reporter", error_message);

    exit(EXIT_FAILURE);
}
7
  • 1
    Why are there errors in your system unknown at compile time? Surely you know a list of all possible errors that can happen? Commented Jul 4, 2024 at 6:36
  • @Lundin A fair point, but it is needed. However, the name may be a bit misleading. Error unknown represents an error with an unknown traceback, something that was at one point correct and now is not. A very severe error, and one that should never exist in production. It would be entirely my fault as the programmer should it. Commented Jul 4, 2024 at 18:20
  • @Zenais Why use "%d" with u32?. I'd recommend int, "%d" for code and unsigned, "%u" for line. Commented Jul 5, 2024 at 6:23
  • @chux-ReinstateMonica Well, code cannot be negative, so an unsigned value felt the best fit there, although I do agree about my use of format modifiers. Commented Jul 5, 2024 at 6:27
  • @chux-ReinstateMonica I apologize, I don't quite get what you mean. By type, do you mean an unsigned 32-bit integer? Commented Jul 5, 2024 at 6:36

3 Answers 3

12

A string look-up table would be one obvious way to do it.

typedef enum
{
  ERR_NONE,
  ERR_SERVERS_ON_FIRE,
  ERR_LIVE_BEAVERS,

  ERR_N // total number of supported errors
} err_t;

static const char* err_str[] = 
{
  [ERR_NONE]             = "No error",
  [ERR_SERVERS_ON_FIRE]  = "The servers are on fire",
  [ERR_LIVE_BEAVERS]     = "Live beavers in the server room",
};

static_assert(sizeof(err_str)/sizeof(*err_str) == ERR_N, 
              "err_t and err_str are not consistent with each other");

...

void InternalLogError(..., err_t code)
{
  puts(err_str[code]);
}

Or in case avoiding storing the values at separate places must be avoided, the X-macro version:

#define ERR_LIST(X)                                        \
/*  code                 str                           */  \
  X(ERR_NONE,            "No error")                       \
  X(ERR_SERVERS_ON_FIRE, "The servers are on fire")        \
  X(ERR_LIVE_BEAVERS,    "Live beavers in the server room")

typedef enum
{
  #define ERR_T_ENUM(code, str) code,
  ERR_LIST(ERR_T_ENUM)
  
  ERR_N // total number of supported errors
} err_t;

static const char* err_str[] = 
{
  #define ERR_STR_LUT(code, str) [code] = str,
  ERR_LIST(ERR_STR_LUT)
};
Sign up to request clarification or add additional context in comments.

1 Comment

This is a much more pragmatic solution than the quick fix I've used until now. Thank you.
2

For the sake of seeing an alternative, here is a variation on @Lundin's very good answer.
This inverts the use of macros to achieve an analogous result.

// errors.tbl - NB: no header guards!

#ifdef ERR_AS_STR
const char *errStrs[];
static const char *errToStr( int code ) { return errStrs[code]; }
static const char *errStrs[] = {
#   define X(e,s) s
#else
enum {
#   define X(e,s) e
#endif

    X( ERR_NONE,            "No error" ),
    X( ERR_SERVERS_ON_FIRE, "The servers are on fire" ),
    X( ERR_LIVE_BEAVERS,    "Live beavers in the server room" ),
    X( ERR_N, NULL )
#   undef X
};

and

// main.c

#include <stdio.h>

#include "errors.tbl" // once as enums
#define ERR_AS_STR
#include "errors.tbl" // and now as array of strings (and getter)
#undef ERR_AS_STR

int main( void ) {
    for( int i = ERR_NONE; i < ERR_N; i++ ) puts( errToStr( i ) );
    return 0;
}

This is merely a minimal working prototype, to be adapted to suit requirements with regard to the visibility of the enum, the array and the "getter" function.

3 Comments

I have only been working in C for about two months, and as such am fairly ignorant about these topics, but what is the point of a '.tbl' file? I see you noted something about no header guards, but what's the difference?
@Zenais If you want, you could use #include "borkum.riff". It's just a filename... I used .tbl ("table") to indicate its contents and purpose (included twice) are not those of a typical .h file... There are conventions and expectations for filename extensions known to editors and compilers and human readers. In this case, .tbl was my choice for a name; nothing else. Cheers! :-)
Ohhh, okay! That makes sense, in most other languages I've used it's been just like that, no idea why I thought it wouldn't be the same in C. Thanks :)
0

On some project, I decided I want to fix a similar problem. I decided that defining error code and error text in multiple places is not for me, I want to have all in one place. Instead, I decided that I will define error code and description in one place and preprocess the project files to extract the text and generate error codes to text table. Consider the following source code file:

#include <errorgen.h>
int some_library_function() {
     return ERROR_GEN(ERROR_SOME_ERROR, "This is error description")
}

Before compilation, a CMake or python script went through all the files. For each file containing ERROR_GEN, two files were generated - errorgen_list.h and errorgen_str.h. errorgen_list.h contained all the enums collected from ERROR_GEN functions. errorgen_str.h contained a "This is error description", a table to transform error code to a string. The script looks something along:

def get_all_errors_from_sources():
    rereplace = re.compile(
        r'ERROR_GEN\s*\(\s*(.*)\s*,\s*(".*")\s*\)\s*;',
        flags=re.MULTILINE,
    )
    dir = os.path.dirname(__file__)
    errors: List[Err] = []
    for path in Path(dir).glob("**/*.c"):
        for line in open(path).read():
            res = rereplace.findall(line)
            if res:
                errors += [Err(*res)]
    return errors

def prepare_sources(errors):
    enumout = "/* @file */\n"
    for k, v in errors:
        enumout += "\t" + k + ",  /// " + v + "\n"
    msgout = "/* @file */\n"
    for k, v in errors:
        msgout += "\t" + v + ",  // " + k + "\n"
    return (enumout, msgout)

errors = get_all_errors_from_sources()
enumout, msgout = prepare_sources(errors)
with Path("errorgen_list.h", "w") as f:
    f.write(enumout)
with Path("errorgen_str.h", "w") as f:
    f.write(msgout )

Then with the following errorgen.h:

 enum errorgen_errors_e {
    ERRORGEN_OK = 0,
    ERRORGEN_START = -1000,
 #include "errorgen_list.h"
    ERRORGEN_STOP,
 };

 #define ERROR_GEN(a, b)  a

 const char *error_to_str(int);

And the following errorgen.c:

 static const char *const errorsgen_str[] = {
 #include "errorgen_str.h"
 };

 const char *error_to_str(int err) {
     // error handling...
    return errorsgen_str[err - ERRORGEN_START - 1];
 }
  

I could easily keep error code with error string together in the source code. This resulted in multitude of error codes and easier to track them and more readable error codes. The proper dependency has to modelled in the build system, so that the build system knows to generate errorgen_* before compiling the files.

2 Comments

"I decided that defining error code and error text in multiple places is not for me" The example in the answer I posted could get generated out of X macros in that case. That is #define ERROR_LIST(X) X(0, ERR_NONE, "No error") and so on. I didn't post an answer like that because it wasn't a requirement by the OP and X macros can get a little confusing.
I added an X-macro version to my answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.