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.
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.
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
- CIL programming tutorial – The Basics Part I (Dolinka Márk Gergely)
- MSIL Tutorial (codeguru)
More IL in my blog
- Expert .NET 2.0 IL Assembler (book review)
- IL perversions: throwing and catching strings
- Code Metrics: Number of IL Instructions
Pingback:The Morning Brew - Chris Alcock » The Morning Brew #2500
That seems really interesting. Do you get the chance to use it in an existing project? If so where did you apply it?
I’m curious about real world usage as this would give me motivation for learning IL.
Thanks.
I have not used this extension in real projects. There have been few complex cases where I had to write some things in IL just to have better performance. But these cases are really rare.
I remember building a MSIL assembly to have native method for fast Byte[] copy. I needed this to do some image transformation. AFAIK the cpblk MSIL instruction has no equivalent in C#. This gist can illustrate a concrete scenario of this : https://gist.github.com/odinhaus/8942b8e0455cf9e930d93414edc0ee47
Thanks, Cyril!
During my object to object mapper experiment I also used IL but context was different – I emitted IL code dynamically at runtime. I documented my experiment and it can be found here: http://gunnarpeipman.com/series/object-to-object-mapper/
Will you port this to visual studio 2022?
Erik, there’s nothing I can do about this extension as I’m not author of it. You can keep your eye on this extensions here: https://marketplace.visualstudio.com/items?itemName=ins0mniaque.ILSupport Latest supported version today is VS2019.