Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the Conditional attribute work?

I have some helper methods marked with [Conditional("XXX")]. The intent is to make the methods conditionally compile when only the XXX conditional compilation symbol is present. We're using this for debugging and tracing functionality and it works quite well.

During my research on how the conditional compilation works, I found several sources stating methods tagged with the Conditional attribute will be placed in the IL but calls to the methods will not be executed.

How does code get compiled into IL but not executed? How can I verify the behavior is actually as described? I haven't done much with IL so my skills are little weak in this area.

like image 332
DenaliHardtail Avatar asked May 23 '14 13:05

DenaliHardtail


People also ask

What is attribute C#?

In C#, attributes are classes that inherit from the Attribute base class. Any class that inherits from Attribute can be used as a sort of "tag" on other pieces of code. For instance, there is an attribute called ObsoleteAttribute . This is used to signal that code is obsolete and shouldn't be used anymore.


3 Answers

This is controlled by the compiler. All methods with [Conditional] will still be included in the MSIL, but will include a .custom instance line that details the [Conditional]. At compile time for a method caller the compiler lexes, then does semantic analysis and overload resolution, and finds the .custom instance IL in the method you placed [Conditional] on. It therefore doesn't compile the call.

So: the compiler compiles the target method, but does not compile any call to that method. Note: the method is still there and you could still call it with reflection. See the spec

Calls to a conditional method are either included or omitted depending on whether this symbol is defined at the point of the call. If the symbol is defined, the call is included; otherwise, the call (including evaluation of the receiver and parameters of the call) is omitted.

How can you verify it? Fire up the developer command prompt, type ildasm <enter> and open the relevant dlls/exes. Check out the caller and the called [Conditional] methods. You'll see the called methods have the extra IL with .custom instance, and the caller lines are omitted where you would expect. Try it on a console application with the code below.

Why? It makes conditional calls simpler in some cases than using #if. See Eric Lippert: What's the difference between conditional compilation and the conditional attribute?

class Program
{
    static void Main(string[] args)
    {
        AlwaysEmit();
        DebugEmit();
        VerboseEmit();
    }

    public static void AlwaysEmit()
    {
        Console.WriteLine("Beam me up");
    }

    [Conditional("DEBUG")]
    public static void DebugEmit()
    {
        Console.WriteLine("Kirk out");
    }

    [Conditional("VERBOSE")]
    public static void VerboseEmit()
    {
        Console.WriteLine("Say that again?");
    }
}

And in the corresponding MSIL, VerboseEmit is included, but not called from Main:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  call       void RateScope.SdrApi.UploaderConsoleApp.Program::AlwaysEmit()
  IL_0006:  nop
  IL_0007:  call       void RateScope.SdrApi.UploaderConsoleApp.Program::DebugEmit()
  IL_000c:  nop
  IL_000d:  ret
} // end of method Program::Main

...

.method public hidebysig static void  VerboseEmit() cil managed
{
  .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string)
     = ( 01 00 07 56 45 52 42 4F 53 45 00 00 ) // ...VERBOSE..
  // Code size       13 (0xd)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Say that again\?"
  IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ret
} // end of method Program::VerboseEmit

Bonus points. Look at the console output and the MSIL for this (modify the Emit methods accordingly):

static void Main(string[] args)
{
    int callCount = 0;
    AlwaysEmit(++callCount);
    VerboseEmit(++callCount);
    DebugEmit(++callCount);
    Console.WriteLine("Call count = " + callCount);
    Console.ReadLine();
}
like image 52
Andy Brown Avatar answered Oct 24 '22 13:10

Andy Brown


(Have debated whether this qualifies as an answer or not, but feel it's worth mentioning. If people disagree with DVs or comments, I'll happily delete)

One important feature of the fact that this affects call-sites and not the methods themselves is that this feature works across assemblies, and it's the compilation symbols that are in scope for the call site that affects whether the call is invoked.

So one reason why the actual methods have to be emitted into the compiled assembly is because, at that time, it's not actually known whether the method is going to be invoked.

It's at the later point in time, when the consuming application is being compiled, that we actually know whether the method is used or not. This also means that in a complex solution, where there are multiple consumers at multiple levels, some of the calls may happen (in some projects) and others will not.

like image 23
Damien_The_Unbeliever Avatar answered Oct 24 '22 11:10

Damien_The_Unbeliever


If your conditional somehow does not work and it looks like this in VS:

enter image description here

Make sure you add a Debug compilation symbol in your project Build properties:

enter image description here

You can use DEBUG instead in Debug in your Conditional as well and it would work.

like image 35
Mik Avatar answered Oct 24 '22 13:10

Mik