Using 32 bit (Legacy) DLL on Windows Azure

Op Windows Azure is (bijna) alles mogelijk. Zo ben je niet beperkt tot alleen applicaties op basis van .NET. Maar ook Java, PHP, NodeJS en nog veel meer talen en taaltjes zijn heel goed bruikbaar op het Windows Azure platform, Windows Azure Websites en Windows Azure Virtual Machine.

Windows Azure is in de basis een 64 bits Windows 2008 server park. Maar ook dan ben je niet beperkt tot het gebruik van alleen 64 bits programma’s of dll’s. Dit is erg prettig, want zo hoef je ‘legacy’ software niet perse te verherbouwen. Of deze legacy nu geschreven is een Microsoft taal of anders maakt dan ook weer niet zoveel uit.

Zo hebben wij voor een klant een Windows Azure Proof of Concept uitgevoerd, waarbij we te maken hadden met een 32 bits C++ dll met een soort memory leak. Het resultaat moest wel in de Cloud kunnen draaien en dan ook nog eens netjes schaalbaar zijn.

legacydll

Hoe hebben we dat aangepakt?

Laten we eerst eens kijken naar de C++ dll. Zoals gezegd dit component heeft een soort memory leak. Vooraf nog even de dll was ooit bedoeld en gebruikt in een stand alone desktop applicaties. De dll is niet echt bedoeld om in een gedistribueerde omgeving te werken. Als het component door 2 websites gelijktijdig aangeroepen werd, dan werd het resultaat van beide berekeningen vermengd met elkaar. De dll heeft een stukje shared memory, dat niet per proces uniek is en daardoor vindt de vermenging plaats. In .NET is dit relatief simpel op te lossen. Door gebruik te maken van een Mutex.

 1: public int Calculate32Mutex()

 2: {

 3:     int result = 0;

 4:

 5:     mutex.WaitOne();

 6:     try

 7:     {

 8:         result = Calculate32(); // calls the actual 32bit dll function

 9:     }

 10:     finally

 11:     {

 12:         mutex.ReleaseMutex();

 13:     }

 14:

 15:     return result;

 16: }

Door het gebruik van de Mutex moet iedere aanroep wachten tot de vorige aanroep klaar is. Dat is vervelend als je een behoorlijke load verwacht. Maar we gaan het op Windows Azure gaan hosten. Daar kunnen we mooi schalen en op deze manier de load aan.

Blijft nog wel het 32 bits probleem. Hiervoor zijn 2 oplossingen.

  1. We kunnen de DLL hosten in een 32 bits Console applicatie.
  2. We kunnen de app pool van IIS in 32 bits mode zetten.

Laten we beide bekijken.

Oplossing 1: Het hosten van de DLL in een 32 bits Console applicatie.

We maken een Console applicatie en geven bij de Build opties aan dat het Platform target x86 is. In de main van deze Console applicatie zetten we dan deze code.

 1: static void Main(string[] args)

 2: {

 3:     Uri address = new Uri("net.pipe://localhost/CalculatorService");

 4:

 5:     NetNamedPipeBinding binding = new NetNamedPipeBinding();

 6:     binding.ReceiveTimeout = TimeSpan.MaxValue;

 7:

 8:     using (ServiceHost host = new ServiceHost(typeof(CalculatorDll)))

 9:     {

 10:         var ff = host.AddServiceEndpoint(typeof(ICalcService), binding, address);

 11:         ServiceMetadataBehavior metadata = new ServiceMetadataBehavior();

 12:

 13:         host.Description.Behaviors.Add(metadata);

 14:         host.Description.Behaviors.OfType<ServiceDebugBehavior>()

 15:             .First().IncludeExceptionDetailInFaults = true;

 16:

 17:         Binding mexBinding = MetadataExchangeBindings.CreateMexNamedPipeBinding();

 18:         Uri mexAddress = new Uri("net.pipe://localhost/CalculatorService/Mex");

 19:         host.AddServiceEndpoint(typeof(IMetadataExchange), mexBinding, mexAddress);

 20:

 21:         host.Open();

 22:

 23:         Console.WriteLine("The receiver is ready");

 24:         Console.ReadLine();

 25:     }

 26: }

Met behulp van een named net pipe wordt de service beschikbaar. De methode op deze service bevat dan een call naar de functie in de 32 bits dll.

 1: [DllImport("Win32Project1.dll", SetLastError = true)]

 2: public static extern Int32 Calculate(Int32 delay);

 3:

 4: public int RekenDllExample()

 5: {

 6:     return Calculate(2000);

 7: }

Om dit geheel te kunnen schalen heb ik er voor gekozen om deze achter een ‘normale’ WCF service te hangen. Als je hem rechtstreeks in de Website wil gebruiken, dan kan dat natuurlijk ook. Maar dan wordt het schalen erg beperkt.

Op de WCF service moet dan een koppeling met deze Named Net pipe gemaakt worden. Het handigst is dat te doen in de Global.asax en wel in de Application_BeginRequest methode. Daar heb ik twee methodes: een om de DllHost te starten en een om de koppeling te maken.

 1: string dllHostPath = @"Redist\DllHostx86.exe";

 2: private const int ClientInitTimeOut = 20; // in seconds

 3:

 4: protected void Application_BeginRequest(object sender, EventArgs e)

 5: {

 6:     // Make sure that our dll host is running

 7:     EnsureDllHostRunning();

 8:

 9:     // Make sure the client is connected

 10:     EnsureCalcServiceClientConnected();

 11: }

 12:

 13: private void EnsureDllHostRunning()

 14: {

 15:     Process[] p = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(dllHostPath));

 16:     if (p.Length == 0)

 17:     {

 18:         Application["CalcServiceClient"] = null;

 19:         ProcessStartInfo psi = new ProcessStartInfo(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, dllHostPath).ToString());

 20:         Process dllHost = Process.Start(psi);

 21:     }

 22: }

 1: private void EnsureCalcServiceClientConnected()

 2: {

 3:     CalcServiceClient client;

 4:     client = (CalcServiceClient)Application["CalcServiceClient"];

 5:     if (client == null || client.State != System.ServiceModel.CommunicationState.Opened)

 6:     {

 7:         client = GetCalcServiceClient();

 8:         Application["CalcServiceClient"] = client;

 9:     }

 10: }

 11:

 12: private CalcServiceClient GetCalcServiceClient()

 13: {

 14:     CalcServiceClient serv = null;

 15:

 16:     int retryCount = 0;

 17:     bool connected = false;

 18:     while (retryCount < ClientInitTimeOut * 10)

 19:     {

 20:         try

 21:         {

 22:             EndpointAddress address =

 23:                 new EndpointAddress("net.pipe://localhost/CalculatorService");

 24:             NetNamedPipeBinding binding = new NetNamedPipeBinding();

 25:             binding.ReceiveTimeout = TimeSpan.MaxValue;

 26:

 27:             serv = new CalcServiceClient(binding, address);

 28:             serv.Open();

 29:             if (serv.State == System.ServiceModel.CommunicationState.Opened)

 30:             {

 31:                 connected = true;

 32:                 break;

 33:             }

 34:         }

 35:         catch (Exception e)

 36:         {

 37:         }

 38:

 39:         retryCount++;

 40:         System.Threading.Thread.Sleep(100);

 41:     }

 42:

 43:     if (!connected)

 44:     {

 45:         throw new TimeoutException(

 46:             "Couldn't connect to the calculator service.");

 47:     }

 48:

 49:     return serv;

 50: }

In je project moet je dan ook een Service reference leggen naar de DllHost Console applicatie. Dit is relatief eenvoudig. Je start de Console applicatie op en kies voor Add Service Reference. Hierna zijn er in de Web.config endpoint gegevens toegevoegd. Dit endpoint is net.pipe://localhost/CalculatorService. Maak je geen zorgen over Localhost, want de DllHost applicatie en de WCF service die uiteindelijk het endpoint aanroept, draaien in dezelfde instance. Daarmee is localhost altijd waar, ook in de Cloud.

Verder moet je dan een Folder aan je solution toevoegen waarin je de DllHost console app en de 32 bits dll hebt zitten. Vergeet dan niet om de property ‘Copy to Output Directory’ dan te zetten op ‘Copy Always’ or ‘Copy if newer’.

Oplossing 2: We kunnen de app pool van IIS in 32 bits mode zetten

Deze oplossing is het minst ingrijpend. Enige lastige (ik ben geen IT pro meneer) hoe kun je dat doen vanuit een script. Ten slotte kan je alles op een Windows Azure instance zolang je het maar kunt automatiseren. Als het gescript is, dan kun je met een Startup task het script laten uitvoeren en is het helemaal geregeld.

Het commando om dit te doen is:

 1: REM make apppools 32bit

 2: %windir%\system32\inetsrv\appcmd set config

 3:     -section:applicationPools

 4:     -applicationPoolDefaults.enable32BitAppOnWin64:true

Dit commando in een startup.cmd bestand. Plaatsen in het desbetreffende project. In de ServiceDefinition.csdef definiëren we de startup task en we zijn klaar.

 1: <Startup>

 2:   <Task commandLine="startup.cmd" executionContext="elevated" taskType="simple" />

 3: </Startup>

Verder moet je dan een Folder aan je solution toevoegen waarin de 32 bits dll staat. Vergeet dan niet om de property ‘Copy to Output Directory’ dan te zetten op ‘Copy Always’ or ‘Copy if newer’.

Nadeel van de oplossingen:

Als er dynamische configuratie nodig is, dan is de Console applicatie het minst handig. Zoals gezegd het is een Console applicatie met een app.config. Default kan deze niet zomaar uit de Service configuration settings lezen. Bij de andere oplossing kan de Webservice er gewoon wel bij.

Testen

Om de twee oplossingen te testen heb ik een frontend gemaakt. Daarop staan 4 knoppen. De eerste twee knoppen roepen een WCF service aan die gebruik maakt van de Dll Host console applicatie. De laatste twee knoppen roepen een WCF service aan die gebruik maakt van AppPool in 32 bits mode. Er staan labels result, waar een 0 of een 1 of hoger in komt. Als het getal 0 is dan werd de rekenmodule aangeroepen en is er niets gemixt met een andere aanroep. Is het getal groter dan 1 dan is het resultaat gemixt met een andere aanroep. Dit kun je testen door twee browser naast elkaar open te zetten en bij gelijktijdig (er is nog tijd) drukken op de knop zal er een 0 en een 1 verschijnen. Tenzij er meerdere bloglezers op hetzelfde moment aan het testen zijn 😉

Als je dat doet met de Mutex knoppen, dan zal voor beide het resultaat 0 zijn.

http://rekenmoduletest.cloudapp.net

clip_image002

Uiteraard wil je dan weten hoe het met de load zit, daar kom ik in een volgende blogpost op terug. Want uiteraard gaan we dat ook via een Cloud service testen.

Bovenstaand verhaal gaat niet alleen op voor Windows Azure, maar feitelijk voor elke 64 bits server omgeving on premise of bij een andere hosting partij.

Referenties: