Programming: Preprocessing
Before code is compiled, the processor must go through a preprocessing step. The preprocessor is a procedure that substitutes text for compilable code. Preprocessor directives are commands that the programmer can give to the preprocessor. Whenever you hit the “compile” button, the preprocessor scans through your code before any compilation actually happens. As it scans through, if it encounters any preprocessor directives, it takes note of it. The preprocessor can do several things, such as replacing text or tokens with functionally identical tokens or controlling which parts of the code are compiled.
This course will use three types of preprocessor directives:
Includes
Macro Expansion
Conditional compiling
An include directive locates a source file and replaces the directive with that source file. In the following example…
#include <stdio.h>
The preprocessor will find the “stdio.h” file and replace that line with the contents of the entire file. Suppose we had a file called “test.h”, which contains the following code:
void doTest1();
void doTest2();
void doTest3();
When the preprocessor scans through the below code...
// This is a comment
#include “test.h” // This line will get replaced!
int main(void) {
// Code goes here
}
The compiler will see this instead:
// This is a comment
void doTest1();
void doTest2();
void doTest3();
int main(void) {
// Code goes here
}
If you use angle brackets (<>) when specifying the file (like in #include <stdio.h>), the preprocessor will search through system libraries to find the file. In this course, you’ll typically use angle brackets to include DriverLib headers. If you use quotation marks (“”) when specifying the file (like in #include “test.h”), the preprocessor will look in the source path instead. This method is usually for user-defined headers.
As covered in a previous section, macro expansion finds each specified occurrence and replaces it with a token. The #define directive is used to create macros, and macros can take two forms: object-like and function-like.
Object-like macro expansion has the following syntax:
#define <identifier> <replacement token list>
What this tells the preprocessor is: “every time you encounter <identifier>, replace it with <replacement token list>.”
#define PARM 2 // Every time you encounter PARM, replace it with 2
#define PI 3.1415 // Every time you encounter PI, replace it with 3.1415
Object-like macros do not take parameters, unlike their function-like counterparts.
Function-like macros have the following syntax:
#define <identifier> (<parameters>) <replacement token list>
Similar to the object-like macro, this essentially reads as: “every time you encounter <identifier>(<parameters>), replace it with <replacement token list>.”
#define MUL2(x) (x * 2) // Every time you encounter MUL2(x), replace it with x * 2
#define PRINT_MESSAGE(a,b) \ // Every time you encounter PRINT_MESSAGE(a,b)...
printf(#a " and " #b "\n") // … replace it with printf(#a “ and “ #b “\n”)
With object-like macros, you can “call” them by using their identifier.
#define ARRAY_LENGTH 3
int myArray[ARRAY_LENGTH] = {1, 2, 3};
In the above example, ARRAY_LENGTH would be replaced by 3 after the preprocessor goes through the code.
With function-like macros, you need to provide a parameter as well as the identifier.
#define PRINT_MESSAGE(a,b) \
printf(#a “ and “ #b “\n”)
PRINT_MESSAGE(hello, how are you?);
In the above example, the message "hello and how are you?" would be printed to the console.
Be careful with using function-like macros. Since the parameters aren’t type-defined, you may get unintended side effects from using them! If your macro is longer than a single line, use the continuation operator (\) to extend the macro to the next line. If you want a parameter to be treated as a string, use the stringize (#) operator.
(Learn more: https://www.tutorialspoint.com/cprogramming/c_preprocessors.htm )
Conditional compilation directives determine which parts of the code are preprocessed and compiled. As the name implies, these directives rely on whether conditions are either true or false. Conditional compilation blocks will start with one of the following directives:
#if
#ifdef
#ifndef
The #if directive works similarly to the if statement. It takes a constant expression, and if the expression is true, all of the code between the #if directive and its matching #else, #elif, or #endif directive is compiled. Otherwise, it is ignored.
The #ifdef directive stands for “if defined.” Similarly, #ifndef means “if not defined.” These directives evaluate identifiers. If the identifier has been defined (or not defined for #ifndef) using the #define directive, then all of the code between the #ifdef (or #ifndef) and its matching #else, #elif, or #endif directive is compiled. Otherwise, it is ignored.
The #else directive works like the else statement. If its matching #if, #ifdef, #ifndef, or #elif is false, then the code following the #else directive until its matching #endif directive is compiled.
The #elif directive works like the else-if statement. If the preceding #if, #ifdef, #ifndef, or another #elif is false, then the #elif directive’s constant expression is evaluated. If true, then all of the code from the #elif directive to its matching #else, #elif, or #endif directive is compiled.
If you’ve used MATLAB, then the #endif directive may look familiar. The #endif directive marks the end of an #if, #ifdef, #ifndef, #else, or #elif directive. Here is an example of how conditional compiling directives can be used:
#if VERBOSE
printf(“result\n”);
#endif
#ifdef DEBUG
/* Debug statements */
#endif
#ifndef HEADER
#define HEADER
#endif
(Learn more: https://www.cs.auckland.ac.nz/references/unix/digital/AQTLTBTE/DOCU_078.HTM )
You can also use other conditional compiling directives such as #error (prints an error message to stderr) or #pragma (provides special directions to the compiler).