Rozmawiając z paroma kolegami po fachu zapytałem ich o czym chcieliby poczytać na blogach programistycznych. Jednym z tematów, który się przewinął, był Common Language Runtime, w skrócie CRL. Wychodząc na przeciw oczekiwaniom chciałbym przedstawić wam trochę wiedzy z samym bebechów .NET-a.

Platforma .NET

Zacznijmy, jak zawsze, od podstaw. Na początku była pustka, potem wielki wybuch… dobra, przyśpieszmy. Czym w ogóle są CLR, IL, JIT? Jeżeli wpadła ci w ręce książka dotycząca .NET (lub inne profesjonalne źródło wiedzy) pewnie natchnąłeś się na poniższy diagram.

Platforma .NET wspiera pisanie programów w wielu językach. Oczywiście C# jest najpopularniejszy, ale musimy pamiętać o wsparciu F# i VB.NET. Za dawnych dziejów wspierane były również inne twory Microsoft-u takie jak np. JScript .NET aczkolwiek nie mam pojęcia, co się dzieje aktualnie ze wsparciem jak i samym językiem. Jeżeli wiesz coś na ten temat daj mi znać w komentarzu. O wielojęzyczności platformy mogą też świadczyć opisy szablonów projektów dostarczonych przy instalacji SDK. Wystarczy wywołać komendę dotnet new -l , a na konsoli pokażą się wszystkie dostępne szablony. Wśród nich będzie kolumna dotycząca jakie języki programowania wspiera dany szablon (z oznaczeniem języka domyślnego).

Na początku .NET Core-a  VB.NET nie dostał wsparcia dla nowo tworzącej się platformy, jednak wraz z wyjściem .NET 5.0 i on zyskał kolejną szanse na przebicie się do szerszego groma programistów.

Język Pośredni

Skoro platforma .NET wspiera wiele języków to w jaki sposób następuje kompilacja do kodu natywnego?

Wszystko dzieje się w kilku krokach. Omówimy je na przykładzie wyżej zamieszczonego diagramu. Otóż pierwszym krokiem, kiedy zdecydujemy się już nacisnąć przycisk Build w naszym IDE, jest kompilacja kodu źródłowego do tzw. kodu pośredniego (Common Intermediate Language). Innymi nazwami jakie możesz spotkać to Microsoft Intermediate Language (MSIL) oraz Intermediate Language (IL). Możesz zadać sobie pytanie – czy zjedlibyśmy hotdoga ze stacji? No i ważniejsze – dlaczego nie następuje kompilacja bezpośrednio do kodu maszynowego? Otóż, kod maszynowy jest dość ciężki w utrzymaniu, różni się w zależności od architektury komputera, a nawet posiadanego procesora. Chcąc uniknąć możliwych problemów powstaje kod, który da się odczytać samemu, może być uruchomiony na każdym systemie, który ma wsparcie dla Common Language Infrastructure (CLI).

Jeżeli zastanawiasz się jak wygląda składnia CLI to spieszę z przykładem. Dzięki możliwością https://sharplab.io/ można bez problemu, na stronie internetowej, zobaczyć jaki zostanie wygenerowany kod CIL a nawet Assembler z kodu C#.
Przy pomocy nowej składni języka C# 9 możemy sprawdzić co zostanie wygenerowane z najprostszego przykładu jaki kiedykolwiek powstał

System.Console.WriteLine("Hello World!");

Tak, wraz z nową wersją tego języka tylko tyle potrzebujemy aby napisać Hello World. Ten przykład skutecznie obrazuje ile rzeczy robi za nas kompilator. Wygenerowany kod CIL będzie dużo bardziej rozbudowany.

.class private auto ansi ''
{
} // end of class 

.class private auto ansi abstract sealed beforefieldinit $Program
    extends [System.Private.CoreLib]System.Object
{
    .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
        01 00 00 00
    )
    // Methods
    .method private hidebysig static 
        void $Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 11 (0xb)
        .maxstack 8
        .entrypoint

        IL_0000: ldstr "Hello World!"
        IL_0005: call void [System.Console]System.Console::WriteLine(string)
        IL_000a: ret
    } // end of method $Program::$Main

} // end of class $Program

Po samej długości kodu widać, że sporo się tutaj zadziało. Przede wszystkim kompilator od razu zinterpretował top level statement i za nas stworzył główną klasę $Program. Z racji, że to .NET, każda klasa musi dziedziczyć po System.Object co jest pokazane w linii 6.
Następnie mamy stworzony przez kompilator bezparametrowy konstruktor i wygenerowaną funkcję statyczną $Main. Dopiero w jej wnętrzu możemy zobaczyć jak wygląda po kompilacji. W 22 linii możemy zauważyć wywołanie operacji ldstr, która wypchnie referencje do wcześniej zdefiniowanego string-a. Co ciekawe już na tym etapie zobaczymy pewne optymalizacje wykonane przez kompilator. Dla przykładu, jeżeli spróbujemy wypisać string-a złożonego z innych stringów:

const string hello = "hello";
const string world = "world";
System.Console.WriteLine(hello + world + "!");

Otrzymamy ten sam kod CIL. Kompilator wykryje “rozwiązanie” tego wyrażenia już na etapie kompilacji i wstawi wynik w odpowiednie miejsce.
Ostatnią rzeczą, którą chciałbym opisać, jest kompilacja w trybie debug i w trybie release. Wszystkie powyższe przykłady były skompilowane w trybie Release. Jeżeli skompilujemy ten sam kod w trybie debug otrzymamy dodatkowo jedna mała rzecz:

IL_0000: ldstr "Hello World!"
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: nop
IL_000b: ret

Mistyczna instrukcja nop, jak sama dokumentacja wskazuje, jest to instrukcja, która nie robi nic ważnego. Jedynie może zmarnować cykle procesora. 😁

CLR (Common Language Runtime) i JIT

W wolnym tłumaczeniu CLR oznacza Środowisko Uruchomieniowe Wspólnego Języka. Nazwa jest dość samo opisująca. Tutaj jest pobierany kod w języku pośrednim, następnie jest kompilowany i uruchamiany jako kod maszynowy. Kompilacja zachodzi przy wykorzystaniu mechanizmu JIT (Just in time compilation). Dzięki temu kod jest kompilowany dopiero przy okazji pierwszego dostępu do niego lub pierwszego wywołania danej funkcji. CLR jest odpowiedzialny również za wiele innych rzeczy, takich jak

  • Zarządzanie pamięcią
  • Czyszczenie pamięci (Garbage Collector)
  • Definicja podstawowych typów danych
  • Format Metadanych
  • Funkcje języka takie jak dziedziczenie, interfejsy, przeciążenia
  • … i wiele wiele innych rzeczy

Kusi opcja dodania własnych języków jako możliwy do uruchomienia na CLR? Nic nie stoi na przeszkodzie. CLR będzie w stanie uruchomić każdy język pośredni, który implementuje specyfikacje ECMA dotyczącą CLI (Common Language Infrastructure i tak, programiści mają za dużo podobnych skrótów). Przykładem jest właśnie kompilator C#.

W ten sposób omówiliśmy cały diagram. Oczywiście jest jeszcze ogrom wiedzy na temat CLR, w które można by się zgłębić. Osobiście polecam prezentacje Adama Furmanka dostępne na Youtube. Znacznie szerzej omawia zagadnienie, dość szczegółowo, jeśli interesuje cie tematyka to na prawdę warto.

Jak zwykle, mam nadzieje, że artykuł się podobał.
Do Następnego!

PS. Spodobał Ci się ten artykuł?

Referencje