Writing IL code on Visual Studio

Microsoft Intermedia Language (MSIL) is .NET assembly language that is standardized under name Common Intermediate Language (CIL). All .NET compilers turn source code to this language. Although we hardly have a situation where we have to write intermedia language (IL) code directly it is still good to know how it works and how it is supported on Visual Studio. This blog post fills the gap and shows how to write IL code on Visual Studio.

Want to learn IL? There are some good resources available for those who want to find out more about IL and understand internals of .NET Framework. Those who want to become gurus can buy excellent book by Serge Lidin “Expert .NET 2.0 IL Assembler”. Those who prefer more soft content can start with online book “C# to IL” by Vijay Mukhi.

IL support for Visual Studio

There are situations when writing some parts of code in IL will give better performance or makes application consuming less resources. Writing IL straight to C# or VB.NET solution is not supported out-of-box in Visual Studio. To have IL support in our solutions we can use excellent extension for Visual Studio – IL Support by ins0mniaque. In case of deeper interest on this extension they have source code available at GitHub. Most of newer Visual Studio versions are supported including Visual Studio 2017.

IL Support extension for Visual Studio 2017

After installing the extension we get new application templates and we can add IL files to our existing solutions.

Application templates

New application templates are copies of some existing ones with difference that IL support is already there.

IL application templates

To get overview of what’s there let’s create console application and see what it contains.

Default IL application

Default console application with IL contains two important files. One of these is Program.cs with Main() method that is entry point of console application. Program class is shown here.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;

namespace ILConsoleApp
{
     class Program
     {
         static void Main(string[] args)
         {
         }

         [MethodImpl(MethodImplOptions.ForwardRef)]
         public static extern int Square(int number);
     }
}

I don’t stop for explanations right now but move to other file – Program.il, where one method is written in IL.


.class public ILConsoleApp.Program
{
  .method public static int32 Square(int32 number) cil managed
  {
     .maxstack 2
     ldarg.0
     dup
     mul
     ret
  } }

This method calculates the square of given number. It is pure IL code. Those who have at least some minor experiences with assembly languages probably understand the code with no explanations. For others here is the commented version.


.class public ILConsoleApp.Program
{
  .method public static int32 Square(int32 number) cil managed
  {
     .maxstack 2   // maximum stack depth used by the function code
     ldarg.0       // loads the first argument to the evaluation stack
     dup           // duplicates the value at the top of the stack
     mul           // pops two values from the stack, multiplies and pushes product to stack
     ret           // returns top value from stack (product of mul)
  } }

As we see then there’s not anything too complicated. We just have to get into assembly language mindset.

Declaration of Square() method

Now let’s see the fancy declaration of Square() method in Program.cs file.


[MethodImpl(MethodImplOptions.ForwardRef)]
public static extern int Square(int number);

As Visual Studio knows nothing about the Square() method defined in IL we have to tell it that there will be method like this but it will be there later. To be more specific then extern keyword tells that there is method called Square() in this class but it is defined elsewhere. MethodImpl attribute with ForwardRef tells that this method is defined later, so there’s no need to look for it in this point.

Trying out the code

Now let’s modify Main() method and call Square() to see if it works.


using System;
using System.Runtime.CompilerServices;

namespace ILConsoleApp
{
     class Program
     {
         static void Main(string[] args)
         {
             Console.WriteLine("4 * 4 = " + Square(4));
             Console.ReadKey();
         }

         [MethodImpl(MethodImplOptions.ForwardRef)]
         public static extern int Square(int number);
     }
}

As you can see then IntelliSense works nice with Square() method and it is like any other method we use in our code. This is the output of our program.

4 * 4 = 16

Behind the compiler

One last thing to do is to peek behind the compiler and see what was produced by it. The code we see in editor and what produced for binary can be very different because of compiler optimizations and syntactic sugar like anonymous types or tricks like extension methods. Links provided take you to my blog posts that illustrate what happens with both of these after compiling.

To see what compiler built for us let’s build the code with Release configuration, remove PDB-file and open it in dotPeek.


// Decompiled with JetBrains decompiler
// Type: ILConsoleApp.Program
// Assembly: ILConsoleApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: FC126C9C-C5B2-4C61-8905-784386252C96
// Assembly location: C:\projects\ILConsoleApp\bin\Release\ILConsoleApp.exe

using System;

namespace ILConsoleApp
{
     internal class Program
     {
         private static void Main(string[] args)
         {
             Console.WriteLine("4 * 4 =" + (object)Program.Square(4));
             Console.ReadKey();
         }
         public static int Square(int number)
         {
             int num = number;
             return num * num;
         }
     }
}

We can clearly see that Square() method is compiled as any other method of Program() class and the fact that it was written in IL doesn’t matter much. Also we don’t see anymore MethodImplAttribute and other specifics that guide compiler.

Wrapping up

There are situations when writing code directly in IL gives us benefits like gaining more performance or consuming less resources. Also we can write our own minimal implementations of popular functions that doesn’t perform so well by default. IL is similar to assembly language. There is no support for it in Visual Studio out-of-box but there are extensions available that bring IL to Visual Studio. Although we have to use some compiler guiding in code, it wasn’t something too hard to do.

References

More IL in my blog



See also

5 thoughts on “Writing IL code on Visual Studio

Leave a Reply

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