De GAC / DLL Hell in C#

  1. Inleiding
  2. dynamic link library – oude versie
  3. dynamic link library – nieuwe versie
  4. De DLL Hell
  5. De GAC
  6. gacutil.exe
  7. De executable
  8. Slot

Inleiding

Het kan gebeuren dat bij de distributie van een assembly versies elkaar “bijten” omdat de aanroepende programmatuur nog gebruik maakt van een oude versie.

We hebben vaak geen grip op het aanroepende programma omdat in de meeste gevallen een andere partij het aanroepende programma geschreven heeft.

Het in goede banen leiden van de verschillende versies van een assembly kunnen we regelen met de Global Assembly Cache (GAC).

up | down

dynamic link library – oude versie

Wat is er zo handig aan de Global Assembly Cache? We zullen het toelichten aan de hand van onderstaand voorbeeld. In een vorige posting over strong named assemblies kwam onderstaande methode ter sprake:

using System.Reflection;
[assembly: AssemblyTitle("geslacht")]
[assembly: AssemblyVersion("1.0.0.1")]
[assembly: AssemblyProduct("Library Bepaal Geslacht")]
[assembly: AssemblyFileVersion("1.0.0.1")]
[assembly: AssemblyCopyright("Copyright ©  2020")]
[assembly: AssemblyDescription("Library om het geslacht te bepalen")]
namespace bibliotheek
{
 public static class Geslacht
 {
  public static string Bepaal
  (char code)
  {
   switch (code)
   {
    case 'M': return "Man";
    case 'V': return "Vrouw";
    case 'T': return "Transgender";
    default: return "Onbekend";
   }
  }
 }     
}

Methode Geslacht.Bepaal() heeft één input parameter die de waarden ‘M’, ‘V’ of ‘T’ kan hebben waarbij afhankelijk van de waarde van de input parameter de tekst ‘Man’, ‘Vrouw’ of ‘Transgender’ wordt geretourneerd. Het versienummer van deze assembly variant is 1.0.0.1.

up | down

dynamic link library – nieuwe versie

We gaan een uitbreiding doen. De methode krijgt een tweede “codeHaarKleur” input parameter die de waarden ‘A’, ‘B’, ‘C’ of ‘D’ kan hebben waarbij een haarkleur n.a.v. die tweede input parameter wordt geretourneerd. Het versienummer van deze assembly variant is 2.0.0.0.

using System.Reflection;
[assembly: AssemblyTitle("geslacht")]
[assembly: AssemblyVersion("2.0.0.0")]
[assembly: AssemblyProduct("Library Bepaal Geslacht")]
[assembly: AssemblyFileVersion("2.0.0.0")]
[assembly: AssemblyCopyright("Copyright ©  2020")]
[assembly: AssemblyDescription("Library om het geslacht te bepalen")]
namespace bibliotheek
{
 public static class Geslacht
 {
  public static string Bepaal
  (char code, 
   char codeHaarKleur)
  {
   string resultaat = "";
   
   // Bepaal geslacht
   switch (code)
   {
    case 'M': 
         resultaat =  "Man ";
         break;
    case 'V': 
         resultaat =  "Vrouw ";
         break;
    case 'T': 
         resultaat =  "Transgender ";
         break;
    default: 
         resultaat =  "Onbekend ";
         break;
   }
   
   // Bepaal haarkleur
   switch (codeHaarKleur)
   {
    case 'A': resultaat = resultaat + 
         "met blond haar.";
         break;
    case 'B': resultaat = resultaat + 
         "met bruin haar.";
         break;
    case 'C': resultaat = resultaat + 
         "met rood haar.";
         break;
    case 'D': resultaat = resultaat + 
         "met zwart haar.";
         break;
    default: resultaat = resultaat +
         "met onbekende haarkleur";
         break;      
   }
   // en het resultaat is...
   return resultaat;
  }
 }     
}

Met de bovenstaande attributen en een key file compileren we het één en ander. Zie de Strong named assemblies posting voor een uitleg over wat strong named assemblies zijn en wat key files zijn.

csc 
/t:library 
/out:C:\Assemblies\v2000\geslacht.dll 
/keyfile:SleutelBestand.snk 
geslacht.cs

Als we na het compileren geslacht.dll met de File Explorer bekijken dan zien we bij de eigenschappen van de .dll de waarden welke we in de code bij de attributen hebben opgegeven. We zien dat we van de assembly een versie 2.0.0.0 hebben gecreëerd en het verschil met de versie 1.0.0.1 is dat de nieuwe versie een tweede invoer parameter gebruikt.

up | down

De DLL Hell

En we hebben met twee versies van de assembly een DLL Hell gecreëerd. Als we de oude versie vervangen door de nieuwe versie dan gaat het fout voor de aanroepende programma’s die de oude versie gebruiken omdat in de nieuwe versie twee input parameters vereist zijn.

Omgekeerd gaat het ook fout als de oude versie met twee input parameters wordt aangeroepen. De aanroepende programma’s zelf snel even aanpassen lukt ook niet als die aanroepende programma’s door andere partijen zijn geschreven. Kortom, we hebben alles in huis voor een “DLL Hell”, maar gelukkig is er de Global Assembly Cache (GAC) die dat allemaal gaat oplossen.

up | down

De GAC

Voor .NET Framework 3.5 en nieuwer wordt voor de Global Assembly Cache (GAC) folder C:\Windows\Microsoft.NET\assembly\GAC_MSIL\ gebruikt. In deze folder komen de verschillende assembly varianten te staan. Voor de oudere .NET Framework versies < 3.5 wordt voor de GAC nog folder C:\Windows\Assembly\ gebruikt. Alleen strong named assemblies worden door de GAC geaccepteerd.

up | down

gacutil.exe

Je kunt de assemblies niet kopiëren en in de desbetreffende GAC folder plakken. Voor het neerzetten van assemblies in de GAC is er de Global Assembly Cache Tool (gacutil.exe).

De voorbeeld assembly in deze post heeft twee varianten en we hebben allebei de varianten gecompileerd met een key file waardoor ze strong typed zijn.

We zetten beide varianten via een Windows Command Script met gacutil.exe als volgt in de GAC.

@echo off
gacutil.exe -i C:\Assemblies\v1001\geslacht.dll
gacutil.exe -i C:\Assemblies\v2000\geslacht.dll
pause

waarbij de volgende meldingen verschijnen als het toevoegen goed verloopt:

Click to enlarge…

De assemblies vinden we terug in GAC folder C:\Windows\Microsoft.NET\assembly\GAC_MSIL\ We hebben de assemblies voorzien van een versienummer en we hebben het één en ander gecompileerd met een key file. Het (positieve) gevolg hiervan is dat als we in de GAC folder kijken we van de assembly niet alleen het versienummer zien, maar ook de public key token.

Click to enlarge…

In de sub folder vinden we uiteindelijk de assembly zelf.

Click to enlarge…

up | down

De executable

We hebben in de GAC twee varianten van de assembly neergezet en we maken een aanroepend programma dat gebruik maakt van één van die varianten. Het aanroepend programma zal gebruik maken van de assembly variant met twee input parameters (versie 2.0.0.0):

using System;
using bibliotheek;
namespace Hoofdprogramma
{
 class Klasse
 {
  public static void Main()
  {
    Console.WriteLine("Over jou:");
    Console.WriteLine("Je bent een " + 
    Geslacht.Bepaal(code:'V',codeHaarKleur:'B'));
    Console.WriteLine(
    "Druk op een willekeurige toets om verder te gaan...");
    Console.ReadKey();
  }
 }    
}

We compileren met csc.exe het aanroepend programma naar een executable en we geven op dat gebruik moet worden gemaakt van de .dll assembly variant met twee input parameters (versie 2.0.0.0):

csc main.cs
/t:exe
/r:C:\Assemblies\v2000\geslacht.dll
/out:programma2.exe

We “runnen” executable programma2.exe en we zien dat de juiste assembly wordt gebruikt. programma2.exe roept methode Geslacht.Bepaal()  aan met de input parameters ‘V’ en ‘B’ waardoor de tekst “Je bent een Vrouw met bruin haar” wordt getoond.

Click to enlarge…

We hebben bij het compileren van programma2.exe opgegeven dat een bepaalde .dll assembly gebruikt moet worden. De assembly moet van een bepaalde versie zijn en het moet een specifieke signatuur hebben. We hebben in een eerdere stap de assemblies in de GAC gezet.

programma2.exe draait en het .NET framework en het besturingssysteem gaan voor het aanroepende programma in de GAC kijken en ze vinden de desbetreffende assembly. De assembly wordt klaargezet voor het aanroepende programma en het aanroepende programma zal na een controle dat de klaargezette assembly inderdaad de juiste assembly is, gebruik maken van de desbetreffende assembly.

up | down

Slot

In deze posting heb ik laten zien hoe we via de GAC (Global Assembly Cache) met meerdere varianten van een assembly om kunnen gaan.

Bij het compileren geef je bij het aanroepende programma op welke strong named assembly precies gebruikt moet worden. Alleen strong named assemblies worden door de GAC geaccepteerd. Het .NET Framework en het besturingssysteem zullen vervolgens in de GAC duiken en de gevraagde assembly zal klaargezet worden voor het aanroepende programma (de assemblies moeten natuurlijk wel van te voren in de GAC gezet zijn wil je dit hele verhaal laten doen slagen).

Het aanroepende programma zal ten slotte nog controleren of de klaargezette assembly inderdaad de juiste assembly is waarna de assembly gebruikt kan worden.

Zeer handy zo’n GAC, maar van een eindgebruiker ga je niet verlangen dat hij/zij met gacutil.exe dingen gaat installeren en gacutil.exe is meer voor ontwikkel- en testomgevingen. Voor een definitieve installatie door/bij een gebruiker definieer je een package dat met de Windows Installer geïnstalleerd gaat worden op de desbetreffende computer.

Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…

up


Laat een reactie achter

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *