How can we have dev-friendly environment-dependent compilation in C#?… thanks to preprocessor directives!
This article is also available on Medium.
Let’s say you’re working on a big project with lots of other devs, and you have a professional delivery system with tests, staging and releases. All of these “versions” of your product probably run in different environments, with their own set of env variables and symbols – for example, you surely have a “dev database” and a “prod(uction) database” that require different login and passwords. And you also probably have a system of logging that is more or less verbose depending on your current environment: in a “dev” app mode, you’d expect more debugs and low-level info than in a final production version.
It seems pretty obvious that you should be using the exact same code at every step of the release, though. So your code needs to be able to handle those different environments. At the same time, it would be nice if it wasn’t a headache to insure this env-compatibility; ideally, we’d like the various env settings to be readable and easily changeable.
There are plenty of ways to take this environment into account in your code. But in C#, there’s something really cool for env-dependent compilation: the preprocessor directives.
A little detour: what are C macros?
If you’ve ever done some C or C++, you’re probably familiar with macros. Those are all these weird lines that you start with a sharp sign
# (and don’t end with a semicolon
;!) and that let you define “variables that aren’t really variables“.
What do I mean by that?
Well, the real trick of C macros is that they are not read by the compiler but by the preprocessor. This particular tool runs just before the compiler: it transforms your C code into another piece of C code (still human-readable) that is very close to the original one but where all the macros have been interpreted.
#include <...> statement is, itself, a preprocessor directive! It simply tells the preprocessor to: “go and fetch the code from this header file, and paste it here in place of the macro”.
Note: this is why, in C++, you have the
#pragma once statement (yet another macro!): it’s a safeguard to prevent the preprocessor from re-importing a header file if it has already been copied into your preprocessed code. Otherwise, you could end up with dozens of copies of your
stdio lib…! 😉
So basically, the preprocessor just plugs in the value of your macro-defined symbols and other preprocessing conditions in the program and removes them from the code once it’s done – it’s a simple “find & replace” step. For example, this snippet of C code has a macro to define a constant,
PI, and then simply prints it out:
After the preprocessor has run, the code has been transformed to this:
You can of course have macros that are more evolved, like functions or conditionals, and you may reuse symbols from previous macros:
In truth, you can do pretty complex computations with those “special variables”. But again – the important thing is that the compiler never sees the macros: when it runs, they have already been “injected” into the code.
Those “kind-of-variables” are not actually used as such in the program: they have their own specific scope and they cannot be mixed with the rest of your program’s variables because they are replaced very early in the entire compilation process.
An overview of C# preprocessing directives
C# preprocessor directives look the same as C/C++ macros (they also start with the
# sign) but they are a bit more restrictive…
In C#, preprocessor directives cannot define complex macros like functions, for example. Also, the conditionals can only perform boolean checks: a given symbol is either defined or undefined. (In comparison, C macros can also use integers)
But despite those restrictions, C# preprocessor directives are still pretty useful – and of course, you can still do boolean operations to combine your symbols!
As explained in the Microsoft docs, there are 4 types of preprocessor directives:
- a nullable context allows you to precisely define if the nullable annotations and warnings are enabled or disabled in a given region of your code
- conditional compilation lets you define specific bits of code that are injected conditionally into the final software depending on a given test
Going back to our previous example of dev/staging/production multiple-environments code, this type of directive is great for swapping between two lines of code depending on the value of your
APP_MODE environment variable. Or it can conditionally hide some debugs if you’re not in the “dev” mode.
- defining symbols is a required utility for conditional compilation, so naturally you can use the
#definedirective to create symbols that are later referenced in your conditionals; note that you can also undefine a symbol using the
- finally, you can also define regions in your code to make it easier to read and collapse in IDEs: this is purely meant to facilitate the dev experience and it doesn’t actually impact the software, but it can be nice for your fellow programmers if they can easily show or hide an entire chunk of your code thanks to a well-placed region
- pragmas allow you to give specific instructions to the compiler, for example to temporarily hide some type of warning or to generate checksums that help with debugging
In my personal experience, C# preprocessor directives are mainly used for conditional compilation – but perhaps you have more examples of the other types you’d like to share? 🙂
An important note about define symbols: they are scoped to the file they are defined in! So you can’t create project globals in this manner.
Some quick examples
Example 1: Defining and undefining symbols
Just to get a clearer idea of how symbols can be used for conditional compilation, let’s see how we can define our own custom symbol, undefine it, and use it in an if/else preprocessor directive.
First, let’s setup a basic console program, with our own custom symbol – we use the
#define directive to create this new
MY_SYMBOL object at the top of the script.
#define need to be done at the very beginning of your script – your IDE will probably help you by giving you an error until you’ve fixed it if you put it “too far” in your code 😉
And then, let’s use this symbol in a conditional and check that we do get the output:
Now, what happens if we undefine the symbol before our piece of code that does conditional compilation?
This time, nothing gets printed to the console, because the
#if directive gets a false result on the check. I’ll admit it’s a pretty silly example, because we have no reason to define something and undefine it a line later; but this shows you how the entire flow would go, if you had to “create” and then “destroy” a symbol.
And we can, of course, extend our if/else directive to handle more cases with the
DEBUG symbol is automatically defined in your C# project and set depending on your build settings (“Debug” or “Release” mode). You can override it with
#undef, like here.
Example 2: Trying to use a new API… or falling back to the older version
One common use case for conditional compilation is to avoid API breaking changes on your dependencies. Say you’re using an API for which you know a breaking change has happened at one point; this means that if you force your code to use the newer version, it might crash on older systems.
Then, checking for your current running environment can allow you to fallback to a different version if need be very easily (this example is directly taken from the Microsoft docs):
Example 3: Doing conditional compilation in Unity
If you’ve ever worked on a Unity game and successfully followed through to actually building it, you might have already had a look at C# preprocessor directive for conditional compiling. Indeed, Unity provides us with specific symbols to know whether we’re working in the Unity editor or on an specific OS. It lets us do some pretty powerful platform-dependent compilation.
For example, these directives are super useful to have some test/debug code run only while you’re in the editor (and not in an actual game build):
And by the way, did you know you can define your own symbols in Unity too? By going to your “Project settings” panel, in the “Player settings” section, you can actually add your own defines to the list, and then use them in your preprocessor directives! 🙂
Preprocessor directives are not something I use everyday in C#, but they’re a very nice and very clear way of distinguishing behaviour depending on your environment. They let you use and easily maintain a “shape-shifting” code that is ultimately preprocessed to a different result thanks to conditional compilation.
You can also better control the warnings and annotations your compiler gives, and help the devs in your team by adding user-defined regions in your code.
What about you: do you use a lot of preprocessor directives in your C# projects? Do you find them useful? Feel free to react in the comments! 🙂