Multi-targeting and the C# and VB compilers

The C# and VB compilers in .NET 4.0 and Visual Studio 2010 have richer multi-targeting capabilities than past versions. The latest compilers can compile code containing many new features while still producing assemblies that run on older versions of the framework.

Multi-targeting from the compiler perspective is the ability to generate assemblies that run on multiple versions of the .NET framework. By and large, previous versions of the compilers would generate assemblies that only work on the version of the framework with which the compilers came. But today’s compilers are more flexible and can generate assemblies that target v4.0 or lower frameworks. In addition to allowing you to compile your v3.5 and earlier code and produce an assembly that targets an earlier version of the framework, there are several language features introduced in v4.0 that can be used in programs targeting v3.5 and lower. For simplicity, I’ll use the v3.5 framework to represent all frameworks earlier than v4.0. To the compilers, the choice of a target framework of the output assembly is made when specifying input reference assemblies (/r) to the compiler. Generally speaking, choosing v3.5 references will result in an assembly that targets v3.5, and choosing v4.0 references results in an assembly targeting v4.0. More on that later.

What distinguishes a language feature as one that can be used on an earlier framework is whether or not it depends upon framework types that do not exist on the target framework. For example, the new C# “dynamic” type can only be used on v4.0 because it requires types from the new Microsoft.CSharp.dll assembly which only ships in v4.0. On the other hand, the new support for optional parameters requires no new types and can be used in programs targeting any framework version. The following new C# features can be used in assemblies targeting v3.5:

  • named parameters
  • optional arguments
  • omit the “ref” keyword when calling a COM method. See more here.
  • generic interface and delegate variance

The last feature, the support for variant generic interfaces and delegates, needs a bit more explanation. Both compilers now perform conversions of generic interfaces and delegates that are marked as variant. Such markings persist in metadata. Conversions involving generic interfaces and delegates with no variant marking are done as they always have been. This is interesting because several commonly used base class library (BCL) interfaces and delegate types are now marked with variant annotations in .NET v4.0, for example IEnumerable<out T>. The annotations of these types really give this feature legs when targeting .NET v4.0. But when targeting the v3.5 framework, you will be supplying BCL references to the compiler that include the definition of IEnumerable<T>, and it will not be marked as covariant. So the compiler will not treat conversions involving IEnumerable<T> as covariant.

There are interesting breaking changes to consider when using the new compilers to target v3.5. Of particular interest is the new way in which C# compiler-generated event accessors are compiled. This post details the changes to the mechanism used by compiler-generated add/remove methods to synchronize access to the compiler-generated delegate field backing the event.

For the curious, here are few details on how multi-targeting works under the hood. The key piece of data in an assembly that determines what framework that assembly targets is the MetaDataRuntimeVersion. That, along with the MetaDataRuntimeVersions of the transitive closure of an assembly’s references establish a lower limit for what runtime an assembly can be activated in. The exact behavior of the CLR loader is configurable, and I don’t pretend to know all of the ins and outs of it. But the compilers’ method for choosing a target framework is simple. The MetaDataRuntimeVersion of the output assembly is set to be the same as the MetaDataRuntimeVersion of the assembly housing System.Object. Typically, this assembly is mscorlib.dll. So if you want to use the v4.0 compiler to build a v3.5 assembly you would do something like this:

%windir%\Microsoft.net\Framework\v4.0\csc.exe /nostdlib+ /noconfig /r:%windir%\Microsoft.net\Framework\v2.0.50727\mscorlib.dll <other v3.5/3.0/2.0 references> foo.cs