Automatic Solution Packaging with the C# Preprocessor

PreprocessorThe C# preprocessor, although much less powerful than the C or C++ preprocessors, still provides the ability to enable or disable lines of code based on project-level flags. In addition, when combined with external PowerShell scripts, the C# preprocessor can be used to automatically spin custom C# solutions with subsets of the target code. This can be particularly useful when providing two versions of the source code, for instance a 32-bit and 64-bit version, or when creating custom source code adaptations for different applications.

While preprocessing in general is better replaced by an object-oriented architecture, judicious use can help reduce overhead and improve application maintainability. For instance, when providing source code for an application, processing directives can be used to automatically package multiple source code flavors from the same base library.

The C# preprocessor syntax is relatively straightforward. Code is included or excluded based on hash-prefixed #if statements:

#if TARGET_32BIT
<code here>
#endif

Boolean operators can be used, and multiple variables can be included in the logical expression:

#if ((TARGET_32BIT || TARGET_64BIT) && !DEBUG)
<code here>
#endif

The variables are defined either on a per-file basis, or one for the entire project:

#define TARGET_32BIT
#undef TARGET_32BIT

Defining a variable sets its value to true, while undefining it sets its value to false. In order to define a variable on a project basis, open the project settings, “Build Tab”, and enter the variable under the “Conditional compilation symbols” area.

With the preprocessor variables and code subsets defined, the project is now ready for packaging. While it may suffice to simply deploy the source code as-is, it is a somewhat inelegant solution to include extraneous code in the final project. A better solution is to apply post-processing through a PowerShell script that actually deletes the extraneous code for a particular application.

The functions that parse a text file and manually apply DEFINE tags are as follows:

Function ApplyIfTrue($type, $filename){
$txt = [IO.File]::ReadAllText($filename)
$txt = ($txt -replace "(?ms)^\s*#if[^\r\n]*!$type[^\r\n]*(.*?)#endif[^\n]*\n", "")
$txt = ($txt -replace "(?ms)^\s*#if[^\r\n]*$type[^\r\n]*(.*?)#endif[^\n]*\n", '$1')
Set-Content -Path $filename -Value $txt
}

Function ApplyIfFalse($type, $filename){
$txt = [IO.File]::ReadAllText($filename)
$txt = ($txt -replace “(?ms)^\s*#if[^\r\n]*!$type[^\r\n]*(.*?)#endif[^\n]*\n”, ‘$1′)
$txt = ($txt -replace “(?ms)^\s*#if[^\r\n]*$type[^\r\n]*(.*?)#endif[^\n]*\n”, “”)
Set-Content -Path $filename -Value $txt
}

The ApplyIfTrue function will run through a code file, and remove code segments that contain “!VARIABLE” in the #if statement. Simultaneously, ApplyIfTrue will remove only the surrounding preprocessor statements around code segments marked for inclusion through “#if VARIABLE”. ApplyIfFalse produces the reverse results of ApplyIf for a variable, processing the code and assuming that the variable is not defined.

The proper use of these statements would be as follows. Assuming that a solution has certain statements marked for 32-bit, others for 64-bit and some for both, the PowerShell script would first create a duplicate copy of appropriate files, and then run the following code for each applicable source file:

[IO.Directory]::SetCurrentDirectory((Convert-Path (Get-Location -PSProvider FileSystem)))
ApplyIfTrue "TARGET_32BIT" "Program.cs"
ApplyIfFalse "TARGET_64BIT" "Program.cs"

The first line initializes the current directory in the .Net IO library. The next line applies the preprocessor statements for the “TARGET_32BIT” variable, removing code that should be excluded based on the processor conditions. Finally, the last line removes any markup based on the “TARGET_64BIT” variable.

Note: the underlying premise of this code is that all the variables are completely decoupled. In order to apply this logic to the code, only the following boolean conditions should be used:

#if (TARGET_32BIT)
#if (!TARGET_32BIT)
#if (TARGET_64BIT)
#if (!TARGET_64BIT)
#if (TARGET_32BIT || TARGET_64BIT)
#if (!TARGET_32BIT && !TARGET_64BIT)
expressions of the format #if(N1 || N2 || N3 || N4 || N5 .... )
expressions of the format #if(!N1 && !N2 && !N3 && !N4 && !N5 .... )

Through automated solution deployment, custom source code spins can be easily produced from a shared code base. C# preprocessor commands, though not as flexible as their C/C++ counterparts, can still be extended to good use through an interim compilation or post-processing step in the build process.

Written by Andrew Palczewski

About the Author
Andrew Palczewski is CEO of apHarmony, a Chicago software development company. He holds a Master's degree in Computer Engineering from the University of Illinois at Urbana-Champaign and has over ten years' experience in managing development of software projects.
Google+

RSS Twitter LinkedIn Facebook Email

Leave a Reply

Your email address will not be published. Required fields are marked *