PersistentEnums

From X-Plane SDK
Jump to: navigation, search

The goal of a string/enum system is to combine the ease of use of C++ enums with the flexibility of text files and strings in a manner that is easy to work with. There may be a more official name for this design pattern, but I don't know it off hand.

The Problem

Enums are useful because the compiler checks them for you, they are ints (and can thus be looped), can be used to look things up in an array, etc. But they have one major weakness: if you insert an enum value in the middle of your code, you get "off by one". This can cause: files to become unreadable (because the enums written to the file don't match what you have now as you read it back), arrays to get out of sync, etc.

Enums are also not necessarily friendly to users...arrays must be used to provide human readable forms of the enum, and that means getting out of sync with the array.

Macros to synchronize arrays

The first improvement we can make is to use macros to let us combine enums and arrays in one place. Here's an example:

/* file master_defs.h */
DEFINE_ENUM(inst_altimeter, "Altimeter")
DEFINE_ENUM(inst_gps, "GPS Receiver")
DEFINE_ENUM(throt_eng_1, "Engine Throttle 1")
/* file defs.h */
#define DEFINE_ENUM(a, b)   a,
enum {
    #include "master_defs.h"
enum_count
};
#undef DEFINE_ENUM
extern const char * enum_descriptions[[enum_count];
/* file defs.cpp */
#define DEFINE_ENUM(a,b) b,
const char * enum_descriptions[[enum_count]=
{
    #include "master_defs.h"
};
#undef DEFINE_ENUM

Here's what's going on: master_defs.h is not a real header - it's just a big list of all information. We use a macro to wrap up both the enum and its human-readable name. Then in defs.h and defs.cpp we separately define the macro to "extract" just one part of each line of master_defs.h.

What's nice about this is that the human readable name and the actual enum are always in sync...we can't have an off-by-one error here.

Macros to Save Enums

Our main problem is that the enum value changes. This isn't a problem as long as we never actually share the enum's value with the outside world. What we need is to somehow convert the enum into some never-changing string before writing it out.

What we can do is use the enum's actual C name (what the programmer sees) as a string in a file to save the enum's value. So if the enum for the joystick's fire button is JOY_FIRE, we actually write "JOY_FIRE" to the file, whether that enum is mapped to 7, 8, or any other int. When we read back our prefs, we convert the string JOY_FIRE into the enum JOY_FIRE.

The trick is to do the same sync'd arrays as above, but using the enum's name as its string. Here's how we can do it:

/* also in defs.cpp */

#define DEFINE_ENUM(a,b) #a,
const char * enum_names[[enum_count+1] =
{
     #include "master_defs.h"
     NULL
};
#undef DEFINE_ENUM

This actually will make a char array of the form { "inst_altimeter", "isnt_gps", "throt_eng", NULL } - in other words an array of strings that match what the programmer sees. enum_names is that array and can be used for conversions between enum values (numbers) and their string names like this:

const char * str_from_enum(int value)
{
    return enum_names[[value];
}

int enum_from_str(const char * value)
{
    int n;
    for (n = 0; n < enum_count; ++n)
    if (!strcmp(value, enum_names[[n]))
         break;
    return n;
}

Now we can convert from a C string to the enum's numeric value and back, making it easy to safely write the string value to a file.

Warning: with this system you can now safely insert new enums in the middle of your list, changing their numeric values, or reorder them. But you __cannot__ rename the enum's C name, because this __is__ the saved value in your files. If this is constricting, then a separate file-safe string should be used, like the "human-readable" names above.

Forward Compatibility

This system is naturally backward compatible; as you add new enums a file with the old enums can still be read (as long as you don't delete enums). But what about forward compatibility or general extensibility? What if you want to handle enums that you've never seen?

The trick is to simply use a variable-sized array like this (written in C++ for simplicity), allowing our 'vocabulary' of enums to grow.

/* This is a vector of all of the enums we've EVER seen...both ones that are
 * built in and new ones that we might 'discover' by reading a file. */
vector<string>	all_enum_names;
/* Init - once at startup we must copy enum_names (our list of built-in enums which
 * comes from our macro trick) to all_enum_names, the variable sized array of everything. */
void init_enums(void)
{
    for (int n = 0; n < enum_count; ++n)
         all_enum_names.push_back(enum_names[[n]);
}

const char * str_from_enum(int value)
{
    return all_enum_names[[value];
}

int enum_from_str(const char * value)
{
    // First look for our string in our list of all the
    // enums we have.
    for (int n = 0; n < all_enum_names.size(); ++n)
    if (!strcmp(value, all_enum_names[[n].c_str()))
         return n;
    // If we do not find this string, it is a new enum.
    // Put it on the back of the array and return the newly
    // minted value for it.
    all_enum_names.push_back(value);
    return all_enum_names.size()-1;
}