Conditioneel Compileren in C#

  1. Inleiding
  2. Broncode
  3. Het .NET framework
  4. Conditioneel compileren
  5. Visual Studio
  6. ILDASM
  7. Slot

Inleiding

Veel software draait in Windows onder de paraplu van het .NET framework. De C# broncode wordt gecompileerd naar een build voor het .NET framework en het framework zorgt ervoor dat er uiteindelijk een stuk machinecode komt voor de computer. We hoeven niet altijd alles in een build te hebben en in deze post gaan we het hebben over hoe conditioneel gecompileerd kan worden met Visual Studio. Het één en ander illustreren we met de Cyberdyne Systems Series 800 Terminator (T-800) uit de Terminator filmreeks.

up | down

Broncode

C# is een programmeertaal en alle programmeertalen gaan terug naar hetzelfde concept uit de jaren ’50 van de vorige eeuw. Bij dat concept gaat men ervan uit dat een computer geprogrammeerd moet worden in een taal dat lijkt op een natuurlijke taal (het Engels) en niet in een machinetaal of iets dat daar sterk op lijkt.

Programmeertalen komen en gaan, maar voor een computer is er sinds de jaren ’50 niet veel veranderd. Een computer denkt nog steeds in ééntjes en nulletjes en het verstaat alleen maar machinetaal.

Zo heeft ook C# een syntax dat zou moeten lijken op een natuurlijke taal (de broncode). De vraag is nu hoe we die broncode gaan omzetten naar iets wat daadwerkelijk gaat draaien op een computer en Microsoft heeft daarvoor het .NET framework in het leven geroepen.

up | down

Het .NET framework

Microsoft kwam in het jaar 2000 met het .NET framework. .NET is een raamwerk waar verschillende programma’s op kunnen draaien en de programma’s kunnen variëren van desktop- tot web applicaties. Het framework kan echter meer dan dat want het framework maakt het voor programma’s ook mogelijk om elkaars functies aan te roepen en om elkaars gegevens uit te wisselen.

C# werd ongeveer in dezelfde tijd als het .NET framework geboren en C# verlegt na 20 jaar nog steeds haar grenzen, want men richt zich nu ook op de niet-Windows besturingssystemen via het .NET Core framework. We beperken ons in deze post tot het Windows besturingssysteem en het .NET framework.

Het volgende gebeurt om iets te laten doen draaien onder het .NET framework:

  • Het proces begint met de omzetting van de C# broncode naar een build door een compiler. We hadden het in de posting over debuggen al gehad over de build en een build bevat MSIL (Microsoft Intermediate Language) welke uit de oorspronkelijke (C#) broncode wordt gecompileerd.
  • De build is in feite een halffabrikaat en dat halffabrikaat wordt weer door de .NET Just In Time-compiler (JIT) omgezet naar machine code. Het is uiteindelijk de machinecode die gaat draaien op de computer.

De omzetting van (C#) broncode naar machine code onder het .NET framework geschiedt dus niet in één, maar in twee stappen hetgeen de volgende voordelen heeft:

  • Je bent niet meer gebonden aan een specifieke programmeertaal omdat meerdere programmeertalen kunnen compileren naar MSIL. .NET wil fatsoenlijk MSIL en uit welke programmeertaal dat komt, dat is .NET om het even.
  • Je kijkt pas bij de omzetting van MSIL naar machinecode naar de eigenschappen van de hardware waarop het programma uiteindelijk zal moeten draaien.

up | down

Conditioneel compileren

We ontwikkelen in een ontwikkelomgeving en we zijn op een gegeven moment klaar met ontwikkelen. Het ziet er naar ons idee allemaal goed uit, het is getest en goed bevonden en er mag een build voor de productieomgeving gemaakt worden.

De IDE (Integrated Development Environment) van Visual Studio biedt een aantal opties om conditioneel te compileren. Stukken code worden dan niet opgenomen in een build.

Je zou zoiets willen omdat bijvoorbeeld bepaalde stukken code uitsluitend bestemd zijn voor ontwikkeldoeleinden en je niet wil dat die code gaat draaien in een productieomgeving.

Een andere reden kan zijn dat bepaalde stukken code alleen maar bedoeld zijn voor specifieke hardware configuraties. De code voor die specifieke hardware hoeft dan niet aanwezig te zijn in de build als we nu al weten dat de desbetreffende build toch niet op die hardware geïnstalleerd zal worden.

up | down

Visual Studio

Laat ons het één en ander toelichten aan de hand van de Terminator filmreeks waarin een kunstmatige intelligentie (Skynet) zelfbewust wordt en tot de conclusie komt dat de mensheid de grootste bedreiging voor haar is. Skynet doet vervolgens haar uiterste best om de mensheid uit te roeien hetgeen met wisselend succes lukt en ze stuurt allerlei machines (Terminators) op de mensheid af om de mensheid definitief het loodje te laten doen leggen.

Cyberdyne Systems Series 800 Terminator (T-800)

Met deze filmreeks in gedachten creëren we vanuit de IDE van Visual Studio een Console App en we geven het de naam “Terminator”. Visual Studio kent standaard twee configuraties voor het maken van een build en je kunt desgewenst een configuratie toevoegen.

Model T-800 was één van de eerste Terminators die door Skynet naar het verleden werd gestuurd om mensen preventief te elimineren zodat de geëlimineerde mensen nooit de toekomstige prominente leiders van het verzet tegen Skynet zullen worden. We definiëren een eigen configuratie en we geven het de naam T-800-ONT:

Configuratie T-800-ONT is een configuratie voor een ontwikkelomgeving. In deze configuratie geven we aan dat er een define DEBUG constant is en een Conditional compilation symbool met de naam ONTWIKKEL.

Bij een configuratie voor de productieomgeving zullen weer andere opties van toepassing zijn waaronder de Optimize code-optie want bij een build in de productieomgeving is performance het belangrijkst en niet allerlei debug overhead zaken die nodig zijn om uit zoeken waar precies fouten optreden. Dat zijn meer zaken voor een ontwikkelomgeving.

In de broncode vinden we een Conditionele compilation symbol terug met de naam ONTWIKKEL (herkenbaar aan de #):

using System;
using System.Diagnostics;
using System.Threading;

namespace Terminator
{
  class Program
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin mission.");

      #if ONTWIKKEL
      Console.WriteLine("{0} Begin opsporen", DateTime.Now);
      #endif
      Thread.Sleep(1000);
      Console.WriteLine("- I found you.");

      #if ONTWIKKEL
          Console.WriteLine("{0} Einde opsporen\r\n", DateTime.Now);
          Console.WriteLine("{0} Begin neutralisatie", DateTime.Now);
      #endif

      Console.WriteLine("- Start terminating...");
      Console.WriteLine("- You will be terminated.");
      Console.WriteLine("- Hasta la vista, baby.");
      Thread.Sleep(1000);
      Console.WriteLine("- Mission accomplished!");

      #if ONTWIKKEL
          Console.WriteLine("{0} Einde neutralisatie \r\n", DateTime.Now);
      #endif

      Einde();
      Console.ReadKey();
    }
    
    [Conditional("DEBUG")]
    public static void Einde()
    {
      Console.WriteLine("Missie voltooid om {0} uur.", DateTime.Now);
    }
  }
}

Functie Einde() in het voorbeeldprogramma is ook de moeite van het vermelden waard. De functie heeft een attribuut [Conditional(“DEBUG”)] en dat heeft als gevolg dat de code alleen uitgevoerd zal worden bij de build configuraties die een Define DEBUG constant hebben.

We compileren het programma met de T-800-ONT configuratie en voeren het programma uit. We zien dat de code tussen de # wordt uitgevoerd omdat de T-800-ONT configuratie een conditional compilation symbol heeft met de naam ONTWIKKEL. We zien ook dat functie Einde wordt uitgevoerd en dat komt omdat de T-800-ONT configuratie een Define DEBUG constant heeft

De T-800 terminator “neutraliseert” naar behoren de desbetreffende persoon en ook het tijdstip wordt getoond waarop het één en ander geschiedt:

We krijgen onderstaand te zien als we het programma compileren met een release configuratie en vervolgens uitvoeren. We zien dat de code tussen de # niet wordt uitgevoerd omdat de release configuratie geen conditional compilation symbol heeft met de naam ONTWIKKEL. Ook functie Einde wordt niet wordt uitgevoerd en dat komt omdat de release configuratie geen Define DEBUG constant heeft:

up | down

ILDASM

ildasm.exe (Intermediate Language DisASsembler) is een tool waarmee je de MSIL (Microsoft Intermediate Language) broncode van een build krijgt te zien. ildasm is één van de tools die inbegrepen zijn bij de Microsoft .NET Framework Software Development Kit (SDK) en je krijgt de tool als je bijvoorbeeld Visual Studio installeert, maar misschien dat het al standaard bij Windows zit. Je weet het snel genoeg door op je computer een scan te doen naar ildasm.exe.

We krijgen onderstaande listing als we de Release Build als volgt met ildasm bekijken: C:\Ildasm /out:listing.txt C:\Terminator\bin\Release\Terminator.exe

.class private auto ansi beforefieldinit Terminator.Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // Code size       87 (0x57)
    .maxstack  1
    IL_0000:  ldstr      "Begin mission."
    IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_000a:  ldc.i4     0x3e8
    IL_000f:  call       void [mscorlib]System.Threading.Thread::Sleep(int32)
    IL_0014:  ldstr      "- I found you."
    IL_0019:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_001e:  ldstr      "- Start terminating..."
    IL_0023:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0028:  ldstr      "- You will be terminated."
    IL_002d:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0032:  ldstr      "- Hasta la vista, baby."
    IL_0037:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_003c:  ldc.i4     0x3e8
    IL_0041:  call       void [mscorlib]System.Threading.Thread::Sleep(int32)
    IL_0046:  ldstr      "- Mission accomplished!"
    IL_004b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0050:  call       valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
    IL_0055:  pop
    IL_0056:  ret
  } // end of method Program::Main

  .method public hidebysig static void  Einde() cil managed
  {
    .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string) 
     = ( 01 00 05 44 45 42 55 47 00 00 )                   
    // ...DEBUG..
    // Code size       21 (0x15)
    .maxstack  8
    IL_0000:  ldstr      "Missie voltooid om {0} uur."
    IL_0005:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    IL_000a:  box        [mscorlib]System.DateTime
    IL_000f:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object)
    IL_0014:  ret
  } // end of method Program::Einde

We zien in de MSIL code dat:

  • alles wat zich tussen de # bevond in de C# broncode niet door de compiler is meegenomen naar de build omdat voor de build een release configuratie is gebruikt die geen gebruik maakt van conditionele compilatie symbool ONTWIKKEL.
  • functie Einde wel deel uitmaakt van de build maar niet wordt uitgevoerd. Verantwoordelijk daarvoor zijn attribuut [Conditional(“DEBUG”)] en het niet geactiveerd zijn van de define DEBUG constant in de release configuratie waarmee we de build hebben gemaakt.

up | down

Slot

In deze posting heb ik laten zien hoe onder het .NET framework een build wordt gemaakt waarbij het .NET framework uiteindelijk ervoor zorgt dat er een stuk machinecode komt dat zal gaan draaien op de desbetreffende computer.

Ik heb, om de materie enigszins verteerbaar te maken een fictief voorbeeld aangehaald uit de Terminator filmreeks waarin Cyberdyne Systems Visual Studio met C# gebruikt voor het ontwikkelen van haar T-800 Terminator machine. Een beetje onwaarschijnlijk, maar wie weet, misschien wordt inderdaad Visual Studio gebruikt voor de ontwikkeling van gevechtsdrones. Hasta la vista, baby…

Vanuit de Visual Studio IDE kan met DEBUG constants, TRACE constants en conditional compilation symbols het één en ander ingesteld worden voor wat de compiler uiteindelijk in de build mag zetten.

Het conditioneel compileren werd nogal eens toegepast om builds te maken voor specifieke hardware platformen en specifieke hardware configuraties. Dit hoeft tegenwoordig steeds minder gedaan te worden omdat je dit steeds meer over kunt laten aan het besturingssysteem en het .NET framework. Je hoeft dan geen aangepaste builds meer te maken.

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

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