LinuxQuestions.org

LinuxQuestions.org (/questions/)
-   Programming (https://www.linuxquestions.org/questions/programming-9/)
-   -   Enum reflection in C (https://www.linuxquestions.org/questions/programming-9/enum-reflection-in-c-4175725325/)

astrogeek 05-22-2023 08:07 PM

Enum reflection in C
 
I am rewriting an application which has a requirement to obtain the name of an enum identifier from it's value, an operation called reflection in many languages. To my knowledge there is no native way to do this in plain old C.

The original applicaiton defined a reflection function for these particular enum values (approx. 90) which consists of a switch with a case for each value which returns a pointer to the identifier names. The case list was maintained manually when the enum definition changed.

I initially wrote a simple sed script to generate the case statements from the header which defines the enum, but still had to copy/paste the result into the source file... not too bad once in a while but that will change frequently during the rewrite and will become a problem.

So I have split out that enum definition into a separate header to ease parsing it, and written a simple script to generate another source file just for the reflection function which is invoked by the Makefile rules when the definition changes. This works well enough and allows the header to be included independently of the reflection source for those modules which do not require the reflection function, and can easily be reused for other enums and projects with minimal configuration, at the expense of the additional Makefile rules and proliferation of header files.

The result is that this header (defs.h)...

Code:

enum myenum {
  MY_RED=100,
  MY_BLUE,
  MY_GREEN,
  MY_BIRD,
  MY_CAT,
  MY_DOG
 };

Produces this source which I write to defs.c...

Code:

/* This source file generated from defs.h by ./make_defs_c.sh Mon May 22 17:30:57 MDT 2023 */

#include "defs.h"

const char *myenum2name(enum myenum type)
{
  switch(type)
      {
        case MY_RED: return "MY_RED";
        case MY_BLUE: return "MY_BLUE";
        case MY_GREEN: return "MY_GREEN";
        case MY_BIRD: return "MY_BIRD";
        case MY_CAT: return "MY_CAT";
        case MY_DOG: return "MY_DOG";
        default: return "Unknown";
      }
}

The relevant Makefile rules for an example application, where make_defs_c.sh is the generating script, are...

Code:

reflect: defs.c reflection.c
        ${CC} -o $@ reflection.c

defs.c: defs.h
        ./make_defs_c.sh > $@

But I have wondered how others approach this problem with plain old C.

A DDG search produces many hits for C++ and C-pound-sign, but nothing very useful for C. Have any of you good folks here at LQ had to do this and/or have any thoughts to share?

Thanks in advance!

pan64 05-22-2023 11:20 PM

yes, I think there is no native way.
I would create a list of values/names in a file (not c or h), and generate all the required c and h files based on that, in a makefile.

GazL 05-23-2023 06:52 AM

You could also do something like this:
Code:

enum example { RED=0, GREEN, BLUE };
char const * const example_names[] = { "RED", "GREEN", "BLUE" };

#define EXAMPLE_NAME(n) ( example_names[(n)] )

Should be easy to auto generate the two declarations, but it's going to be beyond CPP, so you're going to have to use some sort of external tool, whether that be shell-script, awk, or even venerable old 'm4'.

EdGr 05-23-2023 06:58 AM

That can't be done directly, but the C preprocessor can turn symbol names into strings. This is useful for creating an array of symbol names and addresses.

Code:

struct ESTRUCT {
  const char *name;
  void *ptr;
};

#define EXAMPLE(x) {#x, &x}

ESTRUCT mytable[] = {
  EXAMPLE (var1),
  EXAMPLE (var2),
  EXAMPLE (var3)
};

Ed

ntubski 05-23-2023 08:11 AM

Quote:

Originally Posted by EdGr (Post 6432440)
That can't be done directly, but the C preprocessor can turn symbol names into strings. This is useful for creating an array of symbol names and addresses.

I seem to recall seeing a technique similar to this, but with two sets of definitions for the macros where one of them creates the enum { ... } definition, and the other produces the string table.

astrogeek 05-26-2023 11:33 AM

Quote:

Originally Posted by pan64 (Post 6432388)
yes, I think there is no native way.
I would create a list of values/names in a file (not c or h), and generate all the required c and h files based on that, in a makefile.

My reason for generating the c-source from the header file itself was that it seemed it would be more readable/meaningful for myself to maintain the list in native C enum declaration syntax.

May I ask why you would prefer to generate both from a third file?

astrogeek 05-26-2023 11:55 AM

Quote:

Originally Posted by GazL (Post 6432439)
You could also do something like this:
Code:

enum example { RED=0, GREEN, BLUE };
char const * const example_names[] = { "RED", "GREEN", "BLUE" };

#define EXAMPLE_NAME(n) ( example_names[(n)] )

Should be easy to auto generate the two declarations, but it's going to be beyond CPP, so you're going to have to use some sort of external tool, whether that be shell-script, awk, or even venerable old 'm4'.

Mapping the constant values to names via an indexed array is the most straight forward way to provide runtime storage and access. I like the parameterized #define as a convenient way of referencing them.

And I agree, there seems no way of avoiding use of an external script to generate those declarations at compile time. I had hoped to find a clever and simple way of doing this all in the preprocessor but that now seems unrealistic. I was wrong - see clever, simple and elegant preprocessor only method provided by wainamoinen in post #11 below!

A subtle complication of using an indexed array this way is that it introduces a restriction on those constant values in that they need to be zero based and contiguous or nearly so. It woulld be difficult to allow for constants with large offsets from zero, or for disjoint sub-ranges of constant values. Both of these are common and useful and the second, disjoint sub-ranges of constant values is important to my current use (the offset value in my example was intended to suggest this).

This last point kept me focused on use of a switch, actually.

Thanks!

astrogeek 05-26-2023 12:06 PM

Quote:

Originally Posted by EdGr (Post 6432440)
That can't be done directly, but the C preprocessor can turn symbol names into strings. This is useful for creating an array of symbol names and addresses.

Code:

struct ESTRUCT {
  const char *name;
  void *ptr;
};

#define EXAMPLE(x) {#x, &x}

ESTRUCT mytable[] = {
  EXAMPLE (var1),
  EXAMPLE (var2),
  EXAMPLE (var3)
};

Ed

Enum constants are evaluated and replaced by the preprocessor and have no runtime storage of their own, so I am confused by your example. Assuming the x parameter is intended to be the enum identifier name you cannot take its address.

Or did I completely miss your point (very possible!).

But I appreciate the example and reminder of preprocessor quoting capability. Although aware of it, I don't think I have ever made use of it myself... food for thought.

Thanks!

pan64 05-26-2023 01:18 PM

Quote:

Originally Posted by astrogeek (Post 6433039)
My reason for generating the c-source from the header file itself was that it seemed it would be more readable/meaningful for myself to maintain the list in native C enum declaration syntax.

May I ask why you would prefer to generate both from a third file?

Code written in c is not really suitable for this (although not impossible). A simple text file can be parsed easily and generate include and source files together. It's like idl (but much simpler).

EdGr 05-26-2023 03:03 PM

astrogeek - The address is for variables with storage. For an enum, you would use the value instead.

You still have to build the table and write code to look it up. :)
Ed

wainamoinen 05-26-2023 04:42 PM

There is a native way, but requires the use of #defines. The method is called X Macro, not widely known.

Wikipedia has an article about it.

The code is below. Note that the enum entries and the switch cases are "automatically" created.

Code:

#include <stdio.h>


#define ITEMS(I) \
        I(MY_RED  , 100) \
        I(MY_BLUE , 101) \
        I(MY_GREEN, 102) \
        I(MY_BIRD , 103) \
        I(MY_CAT  , 104) \
        I(MY_DOG  , 105)


#define ENUM_ENTRY(name, id) name = id,
enum my_enum
{
        ITEMS(ENUM_ENTRY)
};


#define CASE_ENTRY(name, id) case name: return #name;
const char* MyEnumToName(enum my_enum type)
{
        switch (type)
        {
                ITEMS(CASE_ENTRY)
        };
       
        return "not found";
}


int main(void)
{
        for (int i = 100; i < 106; ++i)
        {
                printf("%s = %i\n", MyEnumToName(i), i);
        }
       
        return 0;
}

The output is:
Code:

$ ./a.out
MY_RED = 100
MY_BLUE = 101
MY_GREEN = 102
MY_BIRD = 103
MY_CAT = 104
MY_DOG = 105


astrogeek 05-26-2023 05:22 PM

Quote:

Originally Posted by wainamoinen (Post 6433087)
There is a native way, but requires the use of #defines. The method is called X Macro, not widely known.

Wikipedia has an article about it.

I had prepared a reply to the previous posts to wrap this thread up, but your post has stopped the presses! I had decided the switch was the way to go and this looks like it might be the way to do it without an external script - thanks!

The only initial drawback I see is that I would have to assign the value to each enum name and would lose the convenience of automatically generated sequential values. I may be able to adjust the ENUM_ENTRY macro to allow for that, or it may not be a problem that needs to be solved.

I'll see what I can do with it and reply again, but thanks for the post, very helpful!

astrogeek 05-26-2023 06:37 PM

If I include the '=' sign as part of the id and change the definition of ENUM_ENTRY as below...

Code:

#define ITEMS(I) \
        I(MY_RED  , =100) \
        I(MY_BLUE , ) \
        I(MY_GREEN, ) \
        I(MY_BIRD , =200) \
        I(MY_CAT  , ) \
        I(MY_DOG  , ) \
        I(MY_MAX, )

#define ENUM_ENTRY(name, id)  name id,

... I get auto generated sequences of constants.

This simple change to your example code...

Code:

int main(void)
{
        const char *name;
        for (int i = 100; i <= MY_MAX; ++i)
        {
                name = MyEnumToName(i);
                if(*name != 'n')
                        printf("%s = %i\n", name, i);
        }

        return 0;
}

...produces the following result for the above enum/switch definition...

Code:

./a.out
MY_RED = 100
MY_BLUE = 101
MY_GREEN = 102
MY_BIRD = 200
MY_CAT = 201
MY_DOG = 202
MY_MAX = 203

That allows the full convenience of auto generated enum constants with no great syntactic burden and no reliance on external scripts.

Thanks, and take a bow!

GazL 05-27-2023 05:56 AM

Yes, that's very clever and something I haven't seen before.

NevemTeve 05-27-2023 10:22 AM

Anyways, if it changes so often, it should be handled run-time (read from file/DB).


All times are GMT -5. The time now is 10:58 AM.