See Also: part 1 and part 2.
In the previous installment, I discussed how lazy I am and shared my general dislike of doing things manually when they can be automated.
So let’s automate…
If you program in C, you are likely familiar with using #define to set a value you can use elsewhere in the code:
#define NUMBER_OF_GUESSES 10
int answer = RandomNumber (100);
for (int try=1; try<=NUMBER_OF_GUESSES; try++)
{
printf ("Try #%d of %d - guess a number from 1-100: ",
try, NUMBER_OF_GUESSES);
guess = InputNumber ();
// ... logic continues ...
}
Now instead of updating multiple places in the file for the number of guesses, only the #define has to be changed. The #define has the advantage of not taking extra code or memory space like a variable would, which is important when you are working on embedded systems with 8K of RAM.
You probably have also seen #defines used for marking code to be included or not included in a program:
#define DEBUG_MODE
void Function ()
{
#if defined(DEBUG_MODE)
printf ("Inside Function()\n");
#endif
// ...logic continues...
}
WIth “#define DEBUG_MODE” there, the printf() will be included. Remove that #define (or comment it out) and it will not.
But #defines can also become macros with parameters, such as this:
#define SNOOZE(x) SleepMs(x*1000)
If you want to sleep for seconds, you could use a macro that turns into the call to the millisecond sleep with the passed-in value multiplied by 1000:
void Function ()
{
printf ("Pausing for 5 seconds...\n");
SNOOZE (5);
// ...logic continues...
}
The C preprocessor will take that “5” and substitute where the “x” is in the replacement text, becoming:
void Function ()
{
printf ("Pausing for 5 seconds...\n");
SleepMs (5*1000);
// ...logic continues...
}
Now if the code is ported to a system with a different sleep call, the #define can be changed to use whatever is available.
I’d expect anyone who has programmed in C has done one or all of these things.
But wait, there’s more!
But a macro does not have to be a simple number, string or function name. It can be a whole block of code that gets substituted. You can put in many lines, just by adding a “\” at the end of the line to continue parsing the next line after it:
#define DISPLAY_COUNTDOWN(x) \
for (int idx=x; idx>=0; idx++) \
{ \
printf ("%d...", idx); \
sleep (1000); /* 1000ms, 1 second sleep */
}
void Function ()
{
printf ("Self-destruct activated...\n");
DISPLAY_COUNTDOWN (10);
// ...logic continues ...
}
And that would be processed to replace the “DISPLAY_COUNTDOWN(10)” with the C code in the #define:
void Function ()
{
printf ("Self-destruct activated...\n");
for (int idx=x; idx>=0; idx++) { printf ("%d...", idx); sleep (1000); /* 1000ms, 1 second sleep */ }
// ...logic continues ...
}
Yeah, it would look ugly if you could see how the C preprocessor puts it in, but it builds and runs and you never see it (unless you specifically look at preprocessed output files).
But that is probably dumb. You should just make a “DisplayCountdown()” function and have it be more normal.
But wait, there’s less dumb…
But in the case of my panel functions, each one of them had a unique panel name and panel identifier, so using a function for them was not really possible. Each one had to be its own function since the functions contained the name of the panel (“PanelMainInit()”, “PanelMainTerm()”, etc.).
But a #define can do that…
#define GENERATE_PANEL_PROTOTYPES(panel_name) \
int panel_name##Init (void); \
int panel_name##GetHandle (void); \
int panel_name##Display (void); \
int panel_name##Hide (void); \
int panel_name##Term (void);
The macro uses “panel_name” as the substitution “variable” passed in, and will place whatever text is there anywhere in the macro where “panel_main” appears. Since I wanted to pass in the filename (without extension) of the panel such as “PanelMain” or “PanelFaults”) and build a function name out of it, I use the ## concatenate feature that will glue the items before and after it together. That macro used like this:
GENERATE_PANEL_PROTOTYPES(PanelMain)
GENERATE_PANEL_PROTOTYPES(PanelFaults)
GENERATE_PANEL_PROTOTYPES(PanelAdmin)
…effectively generates the prototypes like this:
int PanelMainInit (void);
int PanelMainGetHandle (void);
int PanelMainDisplay (void);
int PanelMainHide (void);
int PanelMainTerm (void);
int PanelFaultsInit (void);
int PanelFaultsGetHandle (void);
int PanelFaultsDisplay (void);
int PanelFaultsHide (void);
int PanelFaultsTerm (void);
int PanelAdminInit (void);
int PanelAdminGetHandle (void);
int PanelAdminDisplay (void);
int PanelAdminHide (void);
int PanelAdminTerm (void);
…though it actually looks like one long run-one line for each one if you looked at the pre-processed C output, but the result is the same.
A similar macro could generate the actual functions:
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define GENERATE_PANEL_FUNCTIONS(panelName, panelResourceID) \
static int S_##panelName##Handle = 0; /* Zero is not a valid panel handle. */ \
\
int panelName##Init (void) \
{ \
int panelHandle = 0; \
if (S_##panelName##Handle <= 0) \
{ \
panelHandle = LoadPanel (0, TOSTRING(panelName)".uir", panelResourceID); \
if (panelHandle > 0) \
{ \
S_##panelName##Handle = panelHandle; \
\
panelName##UserInit (panelHandle); \
} \
} \
else \
{ \
panelHandle = S_##panelName##Handle; \
} \
return panelHandle; \
} \
\
int panelName##GetHandle (void) \
{ \
return panelName##Init (); \
} \
\
int panelName##Display (void) \
{ \
int status = UIEHandleInvalid; \
int panelHandle = panelName##Init (); \
if (panelHandle > 0) \
{ \
status = DisplayPanel (panelHandle); \
} \
return status; \
} \
\
int panelName##Hide (void) \
{ \
int status = UIEHandleInvalid; \
if (S_##panelName##Handle > 0) \
{ \
status = HidePanel (S_##panelName##Handle); \
} \
return status; \
} \
\
/* Unload the panel, if valid. */ \
int panelName##Term (void) \
{ \
int status = UIEHandleInvalid; \
if (S_##panelName##Handle > 0) \
{ \
status = DiscardPanel (S_##panelName##Handle); \
if (status == UIENoError) \
{ \
S_##panelName##Handle = 0; \
} \
} \
return status; \
}
That macro would be used like this:
GENERATE_PANEL_FUNCTIONS(PanelMain, PANEL_MAIN)
GENERATE_PANEL_FUNCTIONS(PanelFaults, PANEL_FAULTS)
GENERATE_PANEL_FUNCTIONS(PanelAdmin, PANEL_ADMIN)
…and it would create a fully populated set of functions for those panels.
This allowed me to have a header file that had those macros, such as “PanelMacros.h”, and then have a .c and .h for each panel, or one big file that had them all in it.
// Panels.h
GENERATE_PANEL_PROTOTYPES(PanelMain);
GENERATE_PANEL_PROTOTYPES(PanelFaults);
GENERATE_PANEL_PROTOTYPES(PanelAdmin);
// Panels.c
GENERATE_PANEL_FUNCTIONS(PanelMain, PANEL_MAIN)
GENERATE_PANEL_FUNCTIONS(PanelFaults, PANEL_FAULTS)
GENERATE_PANEL_FUNCTIONS(PanelAdmin, PANEL_ADMIN)
And it worked great! And, if I later decided I wanted to add debugging output or something else, instead of editing one hundred different panel functions I could just modify the macro. For example:
#define GENERATE_PANEL_FUNCTIONS(panelName, panelResourceID) \
static int S_##panelName##Handle = 0; /* Zero is not a valid panel handle. */ \
\
int panelName##Init (void) \
{ \
int panelHandle = 0; \
if (S_##panelName##Handle <= 0) \
{ \
panelHandle = LoadPanel (0, TOSTRING(panelName)".uir", panelResourceID); \
if (panelHandle > 0) \
{ \
DebugPrintf ("Panel %s loaded.\n", TOSTRING(panelName)); \
S_##panelName##Handle = panelHandle; \
\
panelName##UserInit (panelHandle); \
} \
} \
else \
{ \
DebugPrintf ("Panel %s already initialized.\n", TOSTRING(panelName)); \
There are a few things to unpack in this example, such as the use of macros STRINGIFY(x) and TOSTRING(x), but those probably could be their own blog post.
Anyway, if you are lazy, and faced with generating dozens or hundreds of almost identical functions, this macro approach can save a ton of time. The macros I made for my original project, dealing with message functions, are vastly more complex than these, but I figured if I started with those most would run away screaming. (I know I sure would if I had been presented them by a coworker.)
I am sure there will be more to say about this, so perhaps a part 3 will show up.
Until then, I’d love to hear what an experienced C macro programmer has to say about this. I bet there are some better techniques and things I am completely unaware of. I’d love it if you’d share.
Thanks…
Addendum: Since I began writing this post, I have converted about 50 panels at work using a much more complex set of #define macros. They keep evolving as I needed to add support for “parent/child” panels, or extra debugging, or even new functions to check if a panel is displayed at the moment. All I did was update the macro, and the next build could use the new functions. I expect it has already saved me days of typing…