DEV Community

Omogbai Atakpu
Omogbai Atakpu

Posted on

Understanding the .NET Compilation Process (C#)

.NET is a free, open-source, and cross-platform development framework created by Microsoft. It provides a runtime environment, libraries, and tools for building and running applications across Windows, Linux, and macOS. It supports multiple languages, including C#, F#, and Visual Basic .NET.
While several versions of .NET exist, this article focuses on .NET 5 and later.

Cross-platform?

.NET enables developers to write applications that run seamlessly on multiple operating systems without significant modifications. This is achieved through its runtime environment and standardized libraries, ensuring consistent behavior across platforms.

The Compilation Process

When you write and execute C# code, what happens under the hood? Let’s break down the step-by-step process that transforms C# source code into a running application on .NET.

1. Writing the Source Code

A developer writes code in a .NET supported language (C#), using a text editor or IDE like Visual Studio or JetBrains Rider. This code consists of high-level constructs like classes, loops, functions and is saved in a .cs file.

// sample source code
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello, World.");
        Console.WriteLine("Press the 'Enter' key to close the console.");
        // Wait for user to close console
        Console.ReadLine();
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Compilation to CIL

The high-level source code is then translated to Common Intermediate Language (CIL), also known as Microsoft Intermediate Language (MSIL) or simply IL. This low-level, platform-independent instruction set is used by the .NET runtime.
This compilation is handled by Roslyn, the modern C# compiler (formerly csc.exe), which also powers the .NET SDK (dotnet build). It produces an executable (.exe or .dll) containing the CIL code.

// our source code translated into CIL
.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       30 (0x1e)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr      "Hello, World."
  IL_0006:  call       void [System.Console]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  ldstr      "Press the 'Enter' key to close the console."
  IL_0011:  call       void [System.Console]System.Console::WriteLine(string)
  IL_0016:  nop
  IL_0017:  call       string [System.Console]System.Console::ReadLine()
  IL_001c:  pop
  IL_001d:  ret
} // end of method Program::Main
Enter fullscreen mode Exit fullscreen mode

3. Storing in an assembly

The generated CIL code is stored in the platform-independent Portable Executable (PE) format, in a .NET assembly (.exe or .dll). The assembly consists of:

  • Metadata: Includes type information, method signatures, references
  • CIL instructions: The generated CIL code

Our assembly PE file:
Our assembly PE file

4. Common Language Runtime (CLR) and Just-In-Time (JIT) Compilation

The Common Language Runtime (CLR) is the core runtime environment of the .NET framework. It is responsible for managing the execution of .NET programs by providing memory management, security, exception handling, garbage collection, and Just-In-Time (JIT) compilation.
Managed code refers to code that targets the .NET runtime and is managed by the CLR.
Unmanaged code, as you may have guessed, is the opposite. It does not target the .NET runtime and is not managed by the CLR. Instead, it is executed directly by the machine. This means it is platform-specific and must be explicitly written for the intended OS. Unmanaged code can also be less secure.
At runtime, the CLR takes the managed code from the assembly and compiles it into native machine code specific to the underlying hardware and operating system using the JIT (Just-In-Time) Compiler. This happens dynamically at runtime, ensuring the application can execute efficiently across different platforms.

This cross-platform feature of the .NET runtime allows developers to write a single piece of code that is efficiently compiled and executed on different kinds of machines.

Here’s an image that illustrates this quite clearly:

Managed vs Unmanaged Code flowchart
Source: Stackoverflow

5. Execution

After the JIT compilation, the machine code is executed by the CPU. In this case, ‘Hello, World.” is printed to the console. The CLR also provides memory management (via garbage collection), exception handling, and security features to ensure safe execution.

Improvements

.NET also supports Ahead-Of-Time (AOT) compilation. Unlike JIT compilation, where code is compiled at runtime, AOT compiles the source code into native machine code before execution. This leads to faster startup times and reduced runtime overhead.
AOT apps have smaller memory footprints and can run on machines without the .NET runtime installed. AOT compilers include:

  • .NET’s Native AOT
  • ReadyToRun (R2R)

Summary

The source code written in an IDE undergoes several stages before execution. From high-level source code written by the developer, it is:

  1. Compiled into CIL by the compiler (Roslyn for C# or VB.NET).
  2. Stored in an assembly (PE format).
  3. Executed by the CLR using the JIT compiler.
  4. (Optionally) Precompiled using AOT for performance optimization.

The CLR plays a crucial role in managing execution, memory, and security. Its cross-platform compatibility ensures that code written in any .NET-supported language can run on Windows, Linux, and macOS, allowing developers to build applications for diverse environments efficiently.
Mastering the .NET compilation process not only improves application performance but also ensures seamless deployment of scalable applications across various platforms, from cloud to desktop.

Top comments (3)

Collapse
 
ese_atakpu_db31e69d6241c4 profile image
Ese Atakpu

Insightful! Well written

Collapse
 
omogbai profile image
Omogbai Atakpu

Thank you!

Collapse
 
olabayobalogun profile image
OLABAYO BALOGUN

A great read. I learnt new things.