C# – dynamický loading a unloading knižníc DLL

Pred niekoľkými dňami som pri programovaní svojej bakalárskej práce narazil na zaujímavý problém. Jeho riešením som strávil vyše hodiny a tak som sa rozhodol, že týmto článkom možno uľahčím prácu ostatným, ktorí sa s podobným problémom stretnú.

Problém

Predstavme si nasledujúcu situáciu. Máme aplikáciu, ktorá podporuje pluginy (zásuvné moduly). Tieto pluginy sú implementované pomocou DLL knižníc. Vnútorná organizácia týchto DLL súborov nie je z hľadiska tohto článku dôležitá, stačí nám len vedieť, že každý plugin obsahuje niekoľko popisných vlastostí (napríklad PluginName, Version, PluginAuthor apod.).

Hlavná aplikácia dokáže tieto pluginy nájsť a načítať ich za behu pomocou príkazu

Assembly plugin = Assembly.Load(...); 

Týmto volaním sa DLL knižnica nahrá do pamäťového priestoru hlavnej aplikácie a zostane tam, až kým aplikácia neskončí. Pre hlavnú aplikáciu je toto správanie vhodné – ak raz nájdem plugin, s najväčšou pravdepodobnosťou ho budem potrebovať po celý čas behu aplikácie.

Problém nastane v momente, keď budeme chcieť napísať pomocnú aplikáciu na správu pluginov. Táto pomocná aplikácia musí vedieť nájsť nainštalované pluginy, zistiť o nich podrobnosti (meno autora, verziu) a zobraziť tieto informácie užívateľovi. A užívateľ sa môže rozhodnúť plugin odinštalovať.

Pozorný čitateľ už problém asi vidí. Ak musí manažér pluginov zisťovať hodnoty vlastností PluginAuthor alebo Version, musí plugin najprv načítať. Ak by sme na toto načítanie zvolili rovnaký postup ako v hlavnej aplikácii (Assembly.Load(…)), DLL knižnice by sa načítali do pamäťového priestoru manažéra, odkiaľ ich nie je možné odstrániť inak, ako ukončením celej aplikácie. Skrátka a dobre, pluginy by sme nedokázali odinštalovať (nie je možné zmazať DLL knižnicu načítanú v pamäti).

Teória riešenia

.NET Framework nám ponúka isté riešenie tohto problému, aj keď sa toto riešenie nedá nazvať práve elegantným. Každá aplikácia môže vytvárať takzvané aplikačné domény. Aplikačné domény sú v .NET Frameworku reprezentované triedou AppDomain (namespace System).

Dokumantácia na stránkach MSDN hovorí o triede AppDomain nasledovné:

[AppDomain class] Represents an application domain, which is an isolated environment where applications execute.

“[Trieda AppDomain] predstavuje aplikačnú doménu, čo je izolované prostredie, v ktorom bežia aplikácie.”

Application domains, which are represented by AppDomain objects, help provide isolation, unloading, and security boundaries for executing managed code.

“Aplikačné domény reprezentované objektmi AppDomain zabezpečujú izoláciu, unloading a bezbečnostné hranice pre vykonávanie manažovaného kódu.”

A nakoniec to najdôležitejšie:

If an assembly is loaded into the default application domain, it cannot be unloaded from memory while the process is running. However, if you open a second application domain to load and execute the assembly, the assembly is unloaded when that application domain is unloaded.

“Ak je assembly načítaná do hlavnej aplikačnej domény, až do ukončenia procesu ju nie je možné odstrániť z pamäte. Ak však na načítanie a vykonanie assembly vytvoríte novú aplikačnú doménu, assembly je z pamäte odstránená v okamihu zrušenia aplikačnej domény.”

A presne toto správanie pre náš manažér pluginov potrebujeme – chceme načítať DLL knižnicu, vykonať s ňou nejaké operácie a zase ju z pamäte odstrániť, aby sme v prípade potreby mohli DLL súbor zmazať.

Na prvý pohľad tak máme k dispozícii všetko, čo potrebujeme, problematika AppDomains je však ešte o čosi zložitejšia.

Načítanie a vykonanie assembly v oddelenej aplikačnej doméne

Môj prvý pokus o vykonanie DLL knižnice v oddelenej aplikačnej doméne vyzeral približne takto:

// snippet
Environment.CurrentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var pluginDomainSetup = new AppDomainSetup
{
   ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
   DisallowCodeDownload = false,
   ApplicationBase = Environment.CurrentDirectory,
   DisallowBindingRedirects = false
}
var pluginDomain = AppDomain.CreateDomain("Plugin Domain", null, pluginDomainSetup);
Assembly plugin = pluginDomain.Load(...);
//kód pracujúci s pluginom
AppDomain.Unload(pluginDomain); // nefunguje podľa očakávaní

Toto riešnie však nefungovalo a DLL knižnica sa po zrušení pluginDomain nedala odstrániť – zostala v pamäti. Po pár minútach hľadania sa mi podarilo nájsť vysvetlenie tohto správania.

Volanie pluginDomain.Load(…) síce načíta zadanú assembly do aplikačnej domény, no vracia objekt typu Assembly, ktorá nie je odvodená od typu MarshalByRefObject – a tak predávame inštanciu načítanej assembly z pluginDomain do hlavnej aplikačnej domény. Tam ale táto assembly nie je nahratá (je len v pluginDomain), takže sa musi načítať. A práve tomu sme sa chceli použitím AppDomain vyhnúť.

Z tohto vyplýva jedno ponaučenie – kód, ktorý pracuje s pluginom sa nesmie nachádzať v hlavnej aplikačnej doméne. Treba ho umiestniť do pluginDomain. To dokážeme zaistiť pomocou .NET Remotingu. Jednoducho do pluginDomain nahráme špeciálnu proxy triedu (s vhodnými metódami), ktorá plugin načíta, zistí podrobnosti a vráti ich bez toho, aby načítaná assembly prekročila hranice pluginDomain.

Táto proxy trieda musí byť odvodená od MarshalByRefObject, aby sme na ňu mohli použiť volanie AppDomain.CreateInstanceFromAndUnwrap(…). Úplne najjednoduchšie riešenie je umiestniť túto proxy triedu priamo do plugin manažéra, aby sme nemuseli operovať s ďalšou DLL knižnicou, ktorá bude vlastne obsahovať len jednu triedu.

Riešenie

Celkové riešenie problému dynamického unloadingu DLL knižníc po zohľadnení tejto “teoretickej prípravy” vyzerá nasledovne:

class PluginDescriptor
{
   public string PluginName { get; internal set; }

   public class AppDomainProxy : MarshalByRefObject
   {
      public string PluginName { get; set; }

      public void LoadAssembly(string path)
      {
         try
         {
           var dll = Assembly.LoadFrom(path);
           PluginName = xyz // zistene z dll
         }
         catch (TypeLoadException)
         { throw; }
         catch (Exception)
         { throw new TypeLoadException(); }
      }
   }

   public PluginDescriptor(string path)
   {
      Environment.CurrentDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
      var pluginDomainSetup = new AppDomainSetup
      {
         ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile,
         DisallowCodeDownload = false,
         ApplicationBase = Environment.CurrentDirectory,
         DisallowBindingRedirects = false
      };
      var pluginDomain = AppDomain.CreateDomain("Plugin Domain", null, pluginDomainSetup);
      try
      {
         var proxy = pluginDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(AppDomainProxy).FullName) as AppDomainProxy;
         if (proxy != null)
         {
            proxy.LoadAssembly(path);
            PluginName = proxy.PluginName; // informacie ziskane v pluginDomain
         }
         else
         {
            throw new TypeLoadException();
         }
      }
      catch (TypeLoadException)
      { throw; }
      catch (Exception)
      { throw new TypeLoadException(); }
      finally
      {
         AppDomain.Unload(pluginDomain); // zrusi pluginDomain a vsetky v nej nacitane kniznice
      }
   }
}

Po vykonaní tohto kódu docielime presne ten efekt, ktorý sme potrebovali – dynamický unload DLL knižníc.

Záver

Na záver len pár slov. Dynamický unload je oveľa zložitejší problém, ako by sa mohlo zdať pri pohľade na dynamický load. Predsa len, Assmebly.Load(…) a hore uvedený kód pre dynamický unload sa svojou komplexnosťou mierne líšia. Tento článok je tak výsledkom vyše hodinového pátrania a výskumu. Snáď to niekomu príde vhod 🙂

Reklamy

One thought on “C# – dynamický loading a unloading knižníc DLL

Pridaj komentár

Zadajte svoje údaje, alebo kliknite na ikonu pre prihlásenie:

WordPress.com Logo

Na komentovanie používate váš WordPress.com účet. Odhlásiť sa / Zmeniť )

Twitter picture

Na komentovanie používate váš Twitter účet. Odhlásiť sa / Zmeniť )

Facebook photo

Na komentovanie používate váš Facebook účet. Odhlásiť sa / Zmeniť )

Google+ photo

Na komentovanie používate váš Google+ účet. Odhlásiť sa / Zmeniť )

Connecting to %s