Korai inicializáció C#-ban

C# esetében, ha egy típus működéséhez valamiféle inicializációra van szükség, akkor azt a típus statikus konstruktorába tehetjük. Ígéret szerint a típus első használatakor, vagy némi idővel a használata előtt a statikus konstruktor kódja lefut.

Mi van azonban akkor, ha egy olyan inicializációs kódot szeretnénk, ami lefut az assembly-nk bármely más kódja előtt?

Vegyük például a következő könyvtárat:

namespace UsefulLibrary
{
    using System;

    // Inicializációs kód, valamiért ennek le kell futni a library
    // más kódja előtt.
    public static class LibraryInitializator
    {
        public static bool IsInitialized { get; private set; }

        public static void Init()
        {
            Console.WriteLine("Library inicializálva");
            IsInitialized = true;
        } // Init()
    } // class LibraryInitializator

    public class Useful
    {
        public Useful()
        {
            if (!LibraryInitializator.IsInitialized)
            {
                throw new InvalidOperationException("A library nincs inicializálva");
            } // if

            Console.WriteLine("A hasznos osztály rendelkezédre áll.");
        } // Useful()
    } // class Useful
} // namespace UsefulLibrary

A UsefulLibrary használata előtt akár milyen oknál fogva a LibraryInitializator.Init() metódust meg kell hívni. Jó esetben ezt a library használója meg is teszi, rosszabb esetben nem. Nézzük például a következő kódot:

using System;

using UsefulLibrary;

class Program
{
    static void Main(string[] args)
    {
        new Useful();
    } // Main()
} // class Program

Mivel a kód nem hívja meg a LibraryInitializator.Init()-et, a program kimenete a következő:

Unhandled Exception: System.InvalidOperationException: A library nincs inicializálva
   at UsefulLibrary.Useful..ctor() in C:\...\Useful.cs:line 22
   at Program.Main(String[] args) in C:\...\Program.cs:line 9

El lehet intézni, hogy az Init() lefusson automatikusan a UsefulLibrary.dll betöltődésekor?

Modul Inicializer

C#-on keresztül nem látszik, de a CLI definiál egy speciális “típust”. Ez minden assembly-ben megvan, sőt, minden modulban (egy assembly több modulból állhat), és megtalálhatóak hozzá a metaadat bejegyzések is. A típus definíciós táblában mindig ez az első elem, jellemzően <Module>, mint típus névvel.

Ennek a speciális “típusnak” metódusai és mezői is lehetnek, igaz, csak statikusak. Valójában ezt a speciális típust nem lehet típusnak nevezni, mégis bizonyos szempontból úgy működik. Amiért ez a típus ki lett találva, az az, hogy a .NET támogatni tudjon globális (azaz nem osztályhoz tartozó) metódusokat, mezőket. Igen, ilyen van a huszonegyedik században is.🙂

Ami érdekes, hogy a “típus” rendelkezik típus konstruktorral. És ami a legjobb, hogy ez a típuskonstruktor a modul betöltése után azonnal le is fut.

A sznobizmus ára

A C#-ot abban az időben tervezték, amikor valamiért sokan azt gondolták, a csúcsa programok nem a csúnya programozók hibája, hanem a csúnya programkonstrukciókat lehetővé tevő programnyelveké. Ekkor születtek olyan programozási nyelvek, amiben nincs goto (vagy átnevezték labeled brake-re, és picit butították), és minden metódusnak osztályhoz kell tartoznia.

Bizonyos szempontból a C#-is ide tartozik, bár szerencsére mérsékeltebb formában. Ez a mérsékletesség azonban most nem segít, nincs tehát mód arra, hogy C#-ban közvetlenül leírjuk a modul inicializátort, ami egy speciális globális metódus.

IL-ben azonban leírhatunk globális metódusokat, és így a modul inicializátort is. Utána már “csak” annyi lesz a feladatunk, hogy valahogy összeházasítsuk a C#-ot és az IL-t. Kezdetnek írjunk egy olyan IL kódot, ami meghívja a modul inicializátorból a UsefulLibrary.LibraryInitializator::Init() metódusát, hiszen azt szeretnénk elérni, hogy a library minden más kód előtt inicializálódjon.

// külső referenciák
.assembly extern mscorlib {}
.assembly extern UsefulLibrary {}

// Ehhez az assembly-hez készítjük ezt a modult
.assembly Initializer {}

// A modulunk neve, amihez ez a kód tartozik
.module Initializer.dll

// A modul inicializátor egy globális statikus konstruktorként jelenik meg
.method public static specialname void .cctor()
{
        // a string referenciája a veremre kerül, 
        // ez lesz a paramétere a WriteLine() metódus hívásnak
	ldstr "Modul inicializátor elindult"
	call void [mscorlib]System.Console::WriteLine(string)

	// A library inicializálása
	call void [UsefulLibrary]UsefulLibrary.LibraryInitializator::Init()
	ret
} // .cctor()

A kis programot el kell menteni pl Initializer.il néven, majd le kell fordírani:

Most tehát van egy assembly-nk, ami ha betöltődik, azonnal inicializálja a UsefulLibrary-t. Ezzel önmagában sajnos nem olyan sokra megyünk, hiszen valaminek be kell tölteni ezt az assembly-t, amihez használni kell belőle valamit, ami használatot a programozónak ugyan úgy explicit le kell írnia, mint ha leírná a Main()-ben kézzel a LibraryInitializator::Init()-et.

Arra lenne szükség, hogy a most létrehozott modul inicializátort belegyúrjuk a UsefulLibrary-ba.

Assembly-k házasítása

Szerencsére található egy ügyes kis program, ami pont ilyen célokat szolgál. Arra képes, hogy ha egy program több assembly-ből áll össze, azokat átszereli egy szem assembly-vé, az assembly file-okat pedig összemerge-eli. Ennek a programnak a neve ILMerge.exe. Nézzük meg, mit kezd a mi helyzetünkkel.

Van tehát egy UsefulLibrary.dll assembly-nk ami a hasznos library kódot tartalmazza, és egy Initializator.dll, ami egy modul inicializátort tartalmaz. Ha a kettő egyesülne, akkor a hasznos library tartalmazná a modul inicializátort, így bármihez is nyúlnak a hasznos library-ban, akkor az assembly-je betöltődésekor az inicializációs kód azonnal lefut. Mergeljük hát őket össze.

Első lépésként egy könyvtárba másoltam a merge-ölendő file-okat, illetve mivel az eredmény assembly nevének ugyanazt szeretném, mint ami az egyik input assembly, egy Result könyvtárba dolgozok, különben az ILMerge megzavarodik, mert ugyanazt a file-t akarja olvasni és írni. A folyamat így néz ki:

Ekkor a result könyvtárban létrejött az assembly, ami tartalmazza a library kódot és a modul inicializátort egyben. Ez az egész macera csak azért történt, hogy meglegyen a szükséges globális metódusunk:

Ennek a globális metódusnak hála, amikor a UsefulLibrary betöltődik, a library azonnal inicializálódik. Ki tudjuk próbálni az eredményt, ha most ezzel az assembly-vel futtatjuk a cikk elején exception-t produkáló kódot:

Tá-dá…🙂

  1. #1 by Robi on 2012. March 25. - 22:57

    Ennyi hekkelés helyett inkább akkor C++ szvsz…

    • #2 by Tóth Viktor on 2012. March 25. - 23:53

      Összességében csak egy pár soros IL kód, aztán egy ILMerge – ha valaki szorgalmas, írhat rá egy tool-t, ami megcsinálja a fenti két lépést. C++/CLI-t nem ismerem. És szerintem sokan vannak ezzel így🙂
      Másik oldalról, tényleg hekkelés, valami komoly indok kellene, hogy ezt éles kódba beletegyem. De lehetőségnek szerintem jópofa.

  2. #3 by László on 2012. March 26. - 13:54

    Szerencse, hogy ítad, hogy ez nem éles projektben kellett, mert nem készítettem be a “tökönbökömmagam” tool-t🙂 Fene se akar IL-t túrni🙂 Mondjuk aki ide jut, megérdemel 2 nagy pofont meg hogy elvegyék a progger licenszt😉

  3. #4 by mjanoska on 2012. March 26. - 19:16

    Szerintem a különböző granularitású initializer-ek használata nem hekkelés … olyan ez, mint az appdomain event-ek, rossz utánérzést hagynak, de csak ugyanazért mint bármi más – globálisak.
    Szerintem … csirkésmaffiózóskovbojosíelmördzsölős

    • #5 by Tóth Viktor on 2012. March 26. - 19:25

      Nekem inkább az vele a bajom, hogy a C# által generált kód alá betolunk ILMerge-el egy speciális metódust. Most ezt azért lehet megtenni, mert a C# compiler nem használja semmire a modul inicializert. De lehet, hogy a következő service pack-kal valami belekerül a compilerbe, ami valamiért már generál oda titokban egy kódot, és akkor megbukik az egész elv, többet nem tudjuk így összerakni a programunkat. Azaz hosszabb távon a karbantarthatósága kérdéses/veszélyes.

  4. #6 by Laci on 2013. October 4. - 12:06

    Szia,

    miért nem csak simán:

    namespace UsefulLibrary
    {
    using System;

    // Inicializációs kód, valamiért ennek le kell futni a library
    // más kódja előtt.
    public static class LibraryInitializator
    {
    public static bool IsInitialized { get; private set; }

    static LibraryInitializator()
    {
    Console.WriteLine(“Library inicializálva”);
    IsInitialized = true;
    } // Init()
    } // class LibraryInitializator

    public class Useful
    {
    public Useful()
    {
    if (!LibraryInitializator.IsInitialized)
    {
    throw new InvalidOperationException(“A library nincs inicializálva”);
    } // if

    Console.WriteLine(“A hasznos osztály rendelkezédre áll.”);
    } // Useful()
    } // class Useful
    } // namespace UsefulLibrary

    Ez a megoldás milyen esetben bukik meg? Miért nem használható?

    • #7 by Tóth Viktor on 2013. October 4. - 14:37

      Szia,
      Ebben az egyszerű esetben az a jó megoldás, amit írsz, sőt, nem is emlékszem, hogy valaha belefutottam volna olyan helyzetbe, amikor ne lett volna elég a statikus konstruktor.
      Ezt a cikket inkább érdekességnek szántam, hogy lám, a .NET mélyén van más jellegű lehetőség is.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: