C dynamic macro choosing

Viewed 348

I want to invoke a macro by building its name during runtime. I want to have a list of macros that follow the same name rule ERROR_MSG_X where X is the error code and to invoke them according to a variable. I have the following function:

void    print_error(int e)
{
    printf("[ERROR] : code (%d)\n\t%s", e, ERROR_MSG_#e); //not proper syntax
}

The header would be something like:

# define ERROR_MSG_1 "Failed to open file.\n"
# define ERROR_MSG_2 "Failed to read file.\n"
# define ERROR_MSG_3 "Failed to execute abc.\n"
...

I tried messing with # and ## but didn't get the results I wanted.

I have a lot of constraints on what I can and can't do such as no external functions, no multiline macros, no parameterized macros, no global variables, etc. Is there a way to calculate the name of the macro with the value passed or do you have another suggestion on how to do this? I could have an array with the msgs and pass it to the function but I really didn't want to do it that way plus I would have to have the error codes sequential or waste memory to store them.

4 Answers

Macros are expanded before compiling, so there is no macro expansion in the compilation iself. They are there to save you typing, to make your code more readable (sometimes) and to avoid error prone C constructs to be less error prone (also sometimes), by controlling all repeated sequences inside the macro definition and allowing you to specify things in only one place. But once the macro expanded code gets into the compiler, there are no macros anymore.

Proposition 1: X macros

It is possible to use the concept of X macros widely used in Linux kernel source code. The idea is to provide multiple expansions of a macro named X:

#include <stdio.h>

// The X-macro
#define LIST_OF_ERRORS                             \
       X(ERROR_MSG_1, "Failed to open file.\n")    \
       X(ERROR_MSG_2, "Failed to read file.\n")    \
       X(ERROR_MSG_3, "Failed to execute abc.\n")

// 1st definition of X to define enum error codes
#define X(e, m) e,
enum {
LIST_OF_ERRORS
};
#undef X

// 2nd definition of X to define cases in a switch
void print_error(int e)
{
#define X(e, m) case e: printf("[ERROR] : code (%d)\n\t%s", e, m); break;
  switch(e) {
    LIST_OF_ERRORS
    default: printf("[ERROR] : unknown code (%d)\n", e);
  }
#undef X
}

int main(void)
{
  print_error(ERROR_MSG_2);
  print_error(ERROR_MSG_1);
  print_error(ERROR_MSG_3);
  print_error(10);

  return 0;
}

Test:

$ gcc err.c
$ ./a.out 
[ERROR] : code (1)
    Failed to read file.
[ERROR] : code (0)
    Failed to open file.
[ERROR] : code (2)
    Failed to execute abc.
[ERROR] : unknown code (10)

N.B.: This partially answers the question as I use a multi-line macro...

Proposition 2: ## operator

In this proposition you need to use "hardcoded" constant error numbers passed as parameter to PRINT_ERROR() macro otherwise the compiler will complain. The compiler will also complain if you pass unknown error numbers.

#include <stdio.h>

#define ERROR_LABEL_0  "Error message#0\n"
#define ERROR_LABEL_1  "Error message#1\n"
#define ERROR_LABEL_2  "Error message#2\n"
#define ERROR_LABEL_3  "Error message#3\n"


#define PRINT_ERROR(e) fprintf(stderr, "[ERROR] : code (%d)\n\t%s", e, ERROR_LABEL_##e)


int main(void)
{
  PRINT_ERROR(2);
  PRINT_ERROR(1);
  PRINT_ERROR(3);

  return 0;
}

Execution:

$ gcc macro.c
$ ./a.out
[ERROR] : code (2)
    Error message#2
[ERROR] : code (1)
    Error message#1
[ERROR] : code (3)
    Error message#3

But if you use a variable or a constant out of range, the compiler complains:

[...]
int main(void)
{
  int err = 2;

  PRINT_ERROR(2);
  PRINT_ERROR(1);
  PRINT_ERROR(3);
  PRINT_ERROR(err); // <---- Compilation error
  PRINT_ERROR(10);  // <---- Compilation error

  return 0;
}

The errors displayed by the compiler:

$ gcc macro.c
macro.c: In function 'main':
macro.c:15:72: error: 'ERROR_LABEL_err' undeclared (first use in this function); did you mean 'ERROR_LABEL_1'?
   15 | #define PRINT_ERROR(e) fprintf(stderr, "[ERROR] : code (%d)\n\t%s", e, ERROR_LABEL_##e)
      |                                                                        ^~~~~~~~~~~~
macro.c:25:3: note: in expansion of macro 'PRINT_ERROR'
   25 |   PRINT_ERROR(err); // <---- Compilation error
      |   ^~~~~~~~~~~
macro.c:15:72: note: each undeclared identifier is reported only once for each function it appears in
   15 | #define PRINT_ERROR(e) fprintf(stderr, "[ERROR] : code (%d)\n\t%s", e, ERROR_LABEL_##e)
      |                                                                        ^~~~~~~~~~~~
macro.c:25:3: note: in expansion of macro 'PRINT_ERROR'
   25 |   PRINT_ERROR(err); // <---- Compilation error
      |   ^~~~~~~~~~~
macro.c:15:72: error: 'ERROR_LABEL_10' undeclared (first use in this function); did you mean 'ERROR_LABEL_1'?
   15 | #define PRINT_ERROR(e) fprintf(stderr, "[ERROR] : code (%d)\n\t%s", e, ERROR_LABEL_##e)
      |                                                                        ^~~~~~~~~~~~
macro.c:26:3: note: in expansion of macro 'PRINT_ERROR'
   26 |   PRINT_ERROR(10);  // <---- Compilation error

as I said in the comments: you offload loading the array to the pre-processor, it's pretty much like writing out the array but it takes less lines in the function.

void print_error(int e){
    const char *errors[] = {ERRORS};
    printf("[ERROR] : code (%d)\n\t%s", e, errors[e]); 
}

and for the ERRORS macro:

# define ERRORS "Failed to open file.\n",\
                "Failed to read file.\n",\
                "Failed to execute abc.\n"

edit: I'd also advise putting the error codes in an enum, that way you can determine the number of errors and make sure a bad error doesn't get called, when doing this I usually write an entire function that turns error codes into strings, but as you said that might be too long.

You could:

void print_error(int e) {
    const char *p = "";
    if (e == 1) p = ERROR_MSG_1;
    else if (e == 2) p == ERROR_MSG_2;
    else if (e == 3) p == ERROR_MSG_3;
    printf("[ERROR] : code (%d)\n\t%s", e, p);
}

or the same with switch case:. I would do:

void print_error(int e) {
    static const char *const strs[] = { ERROR_MSG_1, ERROR_MSG_2, ERROR_MSG_3 };
    const char *p = (1 <= e && (e - 1) < sizeof(strs)/sizeof(*strs)) ? strs[e - 1] : "";
    printf("[ERROR] : code (%d)\n\t%s", e, p);
}

You could write a macro:

#define GET_ERROR_MSG(e)  ( \
((e) == 1) ? ERROR_MSG_1 :
((e) == 2) ? ERROR_MSG_2 :
((e) == 3) ? ERROR_MSG_3 :
"")
printf("[ERROR] : code (%d)\n\t%s", e, GET_ERROR_MSG(e));

A more confusing macro then it's worth it:

#define CASE_ERROR_MSG_(e, x)  ((e) == (x)) ? ERROR_MSG_##x :
#define GET_ERROR_MSG(e)  ( \
   CASE_ERROR_MSG_(e, 1) \
   CASE_ERROR_MSG_(e, 2) \
   CASE_ERROR_MSG_(e, 3)\
   "" )
printf("[ERROR] : code (%d)\n\t%s", e, GET_ERROR_MSG(e));

Either way, you have to enumerate all messages in one place one way or another, for the runtime part to find it.

Related