Trivia about the [Conditional] attribute

The System.Diagnostics.Conditional attribute has been on blog-todo list for a while, and I'm finally getting around to it.  For the quiz-loving folks, here you go:

Quiz:

First, some lingo: I'll say a Conditional Function is a function with a Conditional attribute on it. The conditional function is active (within the scope of a symbol) if the symbol is defined, else it is inactive.

  1. What happens if you place a conditional attribute on Main?
  2. Can the semantics of the conditional attribute change from language to language (is it enforced by the languages or by the runtime)?
  3. Are inactive conditional functions compiled?
  4. Can you invoke conditional functions via a delegate?
  5. Can you invoke conditional functions via Reflection / MethodInfo.Invoke?
  6. Can conditional functions have byref parameters?
  7. What does it mean to place a conditional attribute on an attribute?
  8. If module A references an inactive conditional function in module B, does A need to have access to B at runtime?  At compile time?

 

If you aced that, you can skip the rest of this entry. About half of them could be categorized as trick questions...

 

Here's some background that helps answer those questions.

Conditional attributes can be placed on methods (and attributes in whidbey) to instruct the compiler to conditionally remove calls to the function if a symbol is not defined. This can be useful for debug-only functionality, like Debug.Assert, which has  a Conditional("DEBUG") on it.

Conditional takes a string argument. If that string is defined (as determined by the compiler's preprocessor), then the compiler emits the method call. If the symbol is not defined, C# still compiles the method, but does not compile the calls. 

A quick demo:

Consider the following file x.cs:

 using System;
using System.Diagnostics;

class Program
{
    [Conditional("Test")]
    static void Test()
    {
        Console.WriteLine("'Test' is defined");
    }

    static void Main()
    {
        Console.WriteLine("Start");
        Test();
        Console.WriteLine("End");
    }
}

When compiled without defining 'Test', the method is not executed.

C:\temp>csc x.cs & x.exe
Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.1378
for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727
Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.

Start
End

But when defining 'Test', the method is executed.

C:\temp>csc x.cs /d:Test & x.exe
Microsoft (R) Visual C# 2005 Compiler version 8.00.50727.1378
for Microsoft (R) Windows (R) 2005 Framework version 2.0.50727
Copyright (C) Microsoft Corporation 2001-2005. All rights reserved.

Start
'Test' is defined
End

 

About Conditional Functions:

1. Compiler vs. CLR:

The Conditional attribute is entirely handled by the compiler without any cooperation from the runtime. The method is still jitted normally, but the compiler just doesn't emit the calls if the symbol is not defined.  You can verify this by running ildasm on the output.

However, by including the Conditional attribute in the BCL, the CLR is essentially defining a cross-language protocol, and then leaving it up to the different languages to implement this protocol.

2. Compiler Enforcement:

Since it's a compiler thing, the compiler has to do most of the enforcement to provide the model it wants.

In C#'s case:

  • Symbol and preprocess are determined by compiler, not the jitter. Each compiler can use its own techniques here. Csc.exe allows this to be set on the command line (/define, /d), or in source files via #define. Other compilers could use other techniques (such as environment variables, config files)
  • C# requires that #define is the first whitespace token (see CS1032). Kudos on the consistency. Unlike C++, C# lets you define members in any order. If #defines were allowed in the middle of a file, then the ordering between the #define and the functions calling a [Conditional] would matter.
  • C# is case sensitive about symbol comparisons.
  • The signature must be return void (see CS0578) and can't have Out params (see CS0685). This makes sense because the code needs to compile when the function call is removed. It does allows ref params.
  • C# won't let you directly create a delegate to conditional attribute. (see CS1618).
  • C# normally flags unused locals. However, if a local is only used as an argument to calling a conditional function, then it would be unused when that conditional function is not active. C# does not flag these as unused. 
  • C# still type checks arguments to the condtional function, even when the conditional function is not active.

C#'s decisions here enforce an intuitive behavior: if the conditional functions are side-effect free, then removing them will not change program behavior or compiler warnings. (Exercise to reader:  how would that break if C# didn't have the policy above).

But different compilers could have different policy decisions here.  For example, a compiler could evaluate arbitrarily complex expressions in the condition. MC++ just ignores the attribute.

 

3. CLR things.

Since the method is still compiled in, it can be referenced and invoked by Reflection. For example:

         MethodInfo m = typeof(Program).GetMethod("Test");
        m.Invoke(null, new object[] { });

 

The Conditional attribute just affects the callsite and not the definition, so you can invoke conditional functions from across modules. This is critical for Debug.Assert(), which is defined in system.dll.

Also, other .NET languages could have their own policy about how to handle Conditional attributes. For example, a naive .Net language (such as ILasm) would not even check if a callsite has a conditional attribute, and emit the call regardless.

 

So what if you put a conditional attribute on Main()?

This is a strange case. A compiler could flag an error here since the user's intent is not clear. Does the user want the whole program to be a nop?  In the absence of a compiler error, the behavior here is settled according to the existing rules (and loopholes in the compile). If the CLR makes the call to Main(), then the conditional attribute is ignored.  This is what happens in C#. (See 'What runs before Main()' ) If you use 'void Main()', the the Conditional attribute is ignored in C#. If you use 'int Main()', then you get an error that the conditional must be on type void.

 

What about Conditional on Attributes?

Conditional attributes are basically the same philosophy of conditional functions, but applied to attributes.