Bob Swart (aka Dr.Bob)
Migratie van Win32 naar .NET

Hoe Win32 ontwikkelaars de overstap naar de wereld van .NET kunnen maken met Borland's Delphi 8 voor .NET (en VCL for .NET).

Ook al is het .NET Framework al enkele jaren beschikbaar, voor vele ontwikkelaars blijft het iets nieuws. Maar net als de overstap van 16-bits naar 32-bits Windows is het niet een vraag of je voor .NET gaat ontwikkelen, maar meer de vraag wanneer. Voor de groep early-adapters is het al jaren het geval, en steeds meer mainstream ontwikkelaars komen daarbij. Er is echter een groep ontwikkelaars die te maken heeft met een bekend probleem: onderhoud aan bestaande Win32 toepassingen. Voor deze systemen geldt dat ze op de komende jaren nog zullen voldoen (zeker zolang niet iedere workstation bij klanten de beschikking heeft over het .NET Framework). Eens komt de dag echter dat de klant een 100% safe en managed .NET toepassing wil zien (en dan per direct, uiteraard). En daarmee is het dilema geschetst voor de Win32 ontwikkelaar en het .NET Framework. Wanneer start je de overstap naar .NET, en vooral ook: hoe?

Een mogelijke stap naar .NET kan inhouden het stoppen van werk aan de Win32 ontwikkeling (alleen nog noodzakelijke bug fixes) en de start van nieuwbouw aan een .NET versie van de toepassing. Als je rekening houdt van een ontwikkeltijd van een jaar of meer dan zal de .NET versie gereed zijn rond het moment dat de klant er behoefte aan heeft. Nadeel van deze benadering is dat voor de .NET toepassing vaak helemaal opnieuw begonnen moet worden met programmeren, gebruikmakend van .NET zaken als WinForms, ASP.NET Web Forms en ADO.NET. Hergebruik kan hooguit op technisch ontwerp niveau plaatsvinden (met eventueel de nodige aanpassingen en "wensen" die in de loop der tijd zijn doorgevoerd), maar de meeste bestaande Win32 source code die gebruikmaakt van MFC of de oude ADO of ASP kan in het ronde archief (ook wel prullenbak genaamd).

Delphi 7 voor Win32
Delphi ontwikkelaars hebben vanaf versie 1 de VCL (Visual Component Library) gebruikt, die bovenop de Windows API gebouwd is. Rond Delphi 6 kwam naar CLX (Component Library for X-platform) bij, die bovenop de Qt library is gebouwd, en daarmee compatibiliteit tussen Win32 en Linux (met Kylix) kon bieden. Het succes van CLX is echter nog niet zo groot, mede gezien de kleine omvang van de behoefte aan Linux desktop toepassingen. Als Delphi ontwikkelaar zijn het dus de VCL toepassingen die onder Win32 draaien, en gebruik maken van database access technieken als de BDE (Borland Database Engine), dbExpress (de cross-platform data access laag voor Windows en Linux), InterBase Express (IBX) en ADO. Voor de ontwikkeling van web server toepassingen voor IIS (Internet Information Server) alsmede Apache 1.x en 2.x web servers, beschikt Delphi over WebBroker en WebSnap, en zit de third-party tool IntraWeb in de doos met Delphi 7.

Delphi 8 for .NET
Waar Delphi 7 de laatste versie van Delphi is die gebruikt kan worden voor de ontwikkeling van Win32 toepassingen, daar biedt Delphi 8 for .NET uitsluitend de mogelijkheid om .NET toepassingen te ontwikkelen. Er is overigens reden aan te nemen dat Delphi 9 zowel Win32 als .NET zal ondersteunen, en rond het eind van het jaar zal verschijnen. Maar in dit artikel beperk ik me tot daadwerkelijk beschikbare versies van Delphi 7 en Delphi 8 for .NET. Waar het misschien makkelijk is om overnieuw te beginnen (met de bouw van een .NET toepassing), is dat niet altijd de snelste of goedkoopste oplossing. Nederlanders staan er om bekend dat ze nogal zuinig zijn, wat ons bij uitstek geschikt maakt voor hergebruik van code. En dus migratie van een bestaande Win32 toepassing, in plaats van herbouw.

Migratie voorbereiden met Delphi 7
Los van de VCL vs. WinForms discussie, bestaat Delphi Win32 code uit een heleboel mogelijkheden in de taal zelf die "unsafe" kunnen worden genoemd. Het is dan ook fijn dat Delphi 7 al de mogelijkheid biedt om via een drietal speciale warnings deze constructies op te sporen. Het zijn de Unsafe type, Unsafe code en Unsafe typecast, maar default staan ze uit zoals te zien in Afbeelding 1.

Delphi 7 Project Options met Unsafe Warnings

Om de warnings te krijgen zul je ze dus expliciet aan moeten vinken in deze dialoog (nb: de reden dat deze warnings default uit staan is niet zo gek: wie een Win32 toepassing wil schrijven heeft totaal geen behoefte aan de unsafe warnings). Je kan ze ook expliciet aanzetten in je project door {$WARN UNSAFE_TYPE ON} op te nemen in je source files. Behalve UNSAFE_TYPE kun je ook UNSAFE_CODE en UNSAFE_CAST aanzetten. Pointer is een voorbeeld van een Unsafe type, net als alle pointer types inclusief de PChar. Deze types zullen dus niet werken in .NET, alhoewel ze je in Delphi 8 for .NET toch kan gebruiken mits je de betreffende code dan als "unsafe" markeert. Een voorbeeld daarvan kwam vorige keer al aan de orde bij het bouwen van een (unsafe) .NET assembly die ook vanuit een Win32 toepassing te gebruiken is. Voor Unsafe code geldt dat er niet bewezen kan worden dat er geen memory zal worden overschreven, of det er geen gevaarlijke dingen met het resultaat kunnen gebeuren. Voorbeeld is de aanroep van @ of de Addr operator die het adres van een variabele teruggeeft. Zie ook onderstaande listing voor een klein voorbeeldje waarin zowel Unsafe types als Unsafe code warnings in voorkomen.

  {$WARN UNSAFE_TYPE ON}
  {$WARN UNSAFE_CODE ON}
  {$WARN UNSAFE_CAST ON}

  program D7Demo;
  {$APPTYPE CONSOLE}
  uses
    SysUtils, Windows;

  var
    X: String;
    Y: PChar; // Unsafe type PChar

  begin
    X := 'unsafe type, code & typecast demo';
    Y := @X[1]; // Unsafe code
    MessageBox(0, 'D7Demo', Y,  0); // Unsafe type PChar
  end.
Behalve de warnings bij de declaratie, resulteert ook ieder gebruik van een "unsafe" variabele in een Unsafe type warning. Handig, want zo kun je snel door je code heen lopen. Als voorbeeld van een Unsafe typecase kun je denken aan het casten van een pointer naar een object. En dat kan vakaer gebeuren dan je denkt, want de TList is bijvoorbeeld slechts een list van pointers, en als je er objecten in stopt dan krijg je dus pointers terug. Als je zelf weet wat je doet, kun je die weer hard naar objecten terug casten, met:
  TMyObject(TListItem)
maar dit zal een warning Unsafe typecase opleveren, en deze code compileert niet met Delphi 8 for .NET. De Delphi 7 compiler warnings kunnen je ook vertellen dat het type Real48 niet langer zal worden ondersteund in .NET, net als het oude object keyword (dat nog uit de Turbo Pascal 5.5 tijd stamt). In Delphi 8 for .NET is het alleen nog het class keyword dat je mag gebruiken hiervoor. Daarnaast zijn geheugenallocatie routines als GetMem, FreeMem, ReAllocMem, etc. natuurlijk ook uit den boze in de .NET wereld, waar de Garbage Collector immers al het geheugenwerk voor ons doet. Dit zal leiden tot Unsafe code warnings. Met behulp van de compiler warnings kun je de source code van een bestaand Delphi 7 project al voorbereiden op een migratie naar .NET. Dan blijft alleen de vraag: welke onderdelen van de toepassing zijn daadwerkelijk te migreren, en voor welke onderdelen is er geen andere keus dan opnieuw te beginnen met behulp van .NET technologie?

Delphi 8 for .NET: VCL of niet?
Een keuze die centraal zal staan bij de migratie van een Win32 project naar .NET is de vraag: VCL of niet? Delphi 8 for .NET ontwikkelaars hebben namelijk een keuze tussen WinForms en VCL for .NET. En alhoewel de WinForms class hierachy lijkt op die VCL, zijn er toch belangrijke verschillen, zowel in de classes zelf, hun properties, methodes en events, als in het gebruik (bij de VCL kent ieder component bijvoorbeeld een "Owner" die verantwoordelijk is voor het opruimen van het component - iets wat bij WinForms niet nodig is). Om de beslissing goed te kunnen nemen, is het belangrijk te weten welke onderdelen van VCL ook aanwezig zijn in VCL for .NET (en in de migratie naar het .NET Framework dus zijn meegenomen door Borland). Het white paper "Migrating Borland Delphi apps to the .NET Framework with Delphi 8" op de Borland website gaat in meer detail in op de verschillende VCL componenten en hun beschikbaarheid. In grote lijnen zijn de meeste VCL componenten ook aanwezig in een .NET variant, met uitzondering van de wat meer Win32-specifieke zaken als DDE, COM en ActiveX controls (alhoewel je COM en ActiveX controls wel zelf in Delphi 8 for .NET kunt importeren).

VCL en Databases
Op database gebied geldt dat de Borland Database Engine (BDE) net als dbExpress en InterBase Express (IBX) ook beschikbaar is in VCL for .NET. Als aantekening moet ik wel vermelden dat het bij de BDE alleen om de lokale BDE gaat, met ondersteuning voor dBASE en Paradox. Het SQL Links deel van de BDE (met ondersteuning voor SQL Server, Oracle, IBM DB2, Informix en InterBase) wordt niet langer meer ondersteund - en dbExpress is daar het aangewezen alternatief voor (met drivers voor SQL Server, Oracle, IBM DB2, Informix, InterBase en SQL Anywhere). De ondersteuning voor ADO onder de naam dbGo for ADO heeft de overstap naar .NET niet meegemaakt, waardoor Delphi 7 toepassingen die gebruik maken van ADO - net als SQL Links - toch van een alternatief gebruik moeten maken. Wat voor veel Delphi ontwikkelaars goed nieuws is, is de ondersteuning van de Delphi data module onder .NET. Dit is een faciliteit die echter alleen beschikbaar is in VCL for .NET projecten. Bij een WinForms project kun je het min of meer simuleren via een Component die je aan je project toevoegt (al dan niet via een assembly teneinde hergebruik tussen projecten te bevorderen).

Verschillen tussen VCL en WinForms
Wie een bestaand VCL project wil migreren naar WinForms zal zich moeten voorbereiden op een hoop werk. Het grootste verschil tussen de manier van werken van de VCL (for .NET) designer en de WinForms designer is het .nfm bestand (de .NET tegenhanger van het Delphi .dfm bestand) dat door de VCL designer gebruikt wordt om componenten, de biijbehorende subcomponenten, properties en hun waardes in op te slaan. De WinForms designer zet deze informatie direct in de source code (in de zgn. 'Windows Form Designer generated code' region). Beide benaderingen hebben voor voordelen en nadelen, maar het probleem is dat je bij migratie van een Win32 VCL toepassing naar .NET's WinForms formaat dus je .dfm moet omzetten naar een embedded code variant. En dan ook nog eens rekening moet houden met het feit dat WinForms wel lijkt op de VCL, maar er slechts een beperkt aantal VCL componenten daadwerkelijk zijn af te beelden op WinForms componenten. Ten tijde van de Delphi for .NET preview command-line compiler (onderdeel van Delphi 7, maar slechts bedoeld als preview) werd er een tool genaamd Dfm2Pas beschikbaar gesteld door Borland om eenvoudige VCL projecten met .dfm bestanden om te kunnen zetten naar WinForms equivalente projecten - compleet met source code voor de .dfm inhoud. De laatste versie van Dfm2Pas is van 19 februari 2003, en onderdeel van Delphi for Microsoft .NET Preview Update 3. Dfm2Pas ondersteunt componenten van de Standard, Additional, Common COntrols, Win32, System, Dialogs, Samples, en zelfs de Win 3.1 category (de tabs van het Delphi 7 Component Palette). Wat ontbreekt zijn de data access en data-aware componenten. Aangezien BDE, dbExpress en IBX uitsluitend VCL componenten zijn, en we voor WinForms gebruik moeten maken van ADO.NET (of de Borland Data Provider), is dat niet zo heel erg gek. Het betekent ook dat we altijd de database access laag zullen moeten herschrijven bij migratie van VCL naar WinForms (of ASP.NET Web Forms).

Voorbeeld Migratie
Als voorbeeld van een migratie kunnen we een van de grootste Delphi 7 demo projecten nemen, de IBX Admin toepassing, te vinden in de Delphi7\Demos\Db\IBX\Admin directory. Deze InterBase admin tool bevat 6 forms en gebruikt InterBase Express om met de database te communiceren. Het is een Win32 toepassing die geschreven is voordat het .NET Framework ter sprake kwam, dus we kunnen er verschillende compatibiliteitsproblemen in terugvinden. Als we het AdminTool.dpr project openen in Delphi 8 for .NET, krijgen we meteen de foutmelding dat de System.Drawing.dcuil niet gevonden kan worden. Dit wordt echter automatisch opgelost met het toevoegen van de System.Drawing assembly aan de references lijst.

PChar, String en SringBuilder
Als we op goed geluk gaan compileren krijgen we meteen al 22 compiler errors (en nog meer warnings, maar die kunnen we voorlopig even negeren). Het begint met een aantal invalid typecase errors in de LoginExecute method van frmAdminToolU.pas, allemaal van de volgende vorm:

    i := GetEnvironmentVariable(PChar('ISC_USER'), PChar(User), 0);
Waarbij er een string constante naar een PChar wordt gecast voordat hij aan de GetEnvironmentVariable functie wordt meegegeven. Die eerste cast kan gewoon weg. De tweede cast, van de User variabele ook, maar dan krijgen we een nieuwe foutmelding omdat User van type String is, en de GetEnvironmentVariable verwacht daar een StringBuilder type. Dat is snel aangepast door variable User van type StringBuilder te maken. Dit levert een extra foutmelding op, omdat het type StringBuilder niet bekend is. Dat is te vinden in de System.Text namespace, dus we moeten de System.dll assembly aan onze references lijst toevoegen, en de System.Text namespace aan de uses clause van de betreffende unit. Het is overigens sowieso beter om in .NET een StringBuilder te gebruiken waar je als Delphi gebruiker in Win32 gewoon een String zou toepassing. De reden is dat (de inhoud van) een String in .NET eigenlijk niet kan worden veranderd. En als je dat wel doet, dan maak je eigenlijk een nieuwe kopie van de String, inclusief de aanpassingen. Kost onnodig veel tijd en geheugen, en de StringBuilder is het aangewezen alternatief. Afijn, na het verwijderen van de PChar cast om de string constantes en de User variabele, en het wijzen van User in type StringBuilder, komt de volgende error die aangeeft dat de SetLength niet op een StringBuilder kan worden gebruikt. In plaats daarvan kunnen we de StringBuilder.Create constructor aanroepen met als argument de lengte die we aan SetLength wilde meegeven. Om tenslotte vanuit een StringBuilder variabele weer een String te krijgen kunnen we de .ToString method aanroepen.

Globale Routines
Na het migreren van de LoginExecute methode, krijgen we de meldingen dat CheckIBLoaded en GetIBClientVersion onbekend zijn. Dat waren globale functies in de Win32 versie van IBIntf.pas, maar zijn interface methods geworden in de .NET versie Borland.Vcl.IBIntf.pas. Via de functie GetGDSLibrary kunnen we bij beide methodes komen. Een reden voor het wijzigen van deze functionaliteit is het feit dat globale routines - zoals CheckIBLoaded en GetIBClientVersion - niet eenvoudig te gebruiken zijn in .NET talen als C#. Het is gebruikelijk in .NET om dergelijke routines als interface of class members op te nemen, en dat is dan ook wat er in de unit Borland.Vcl.IBIntf.pas is gebeurd. Gelukkig is de Delphi 8 for .NET compiler zelf ook in staat om ondersteuning te bieden voor andere .NET talen, door een psuedo class genaamd "Unit" te gebruiken voor het plaatsen van alle globale routines en data binnen een unit. Op die manier kun je er toch vanuit een andere .NET omgeving bij. Ik verwijs graag naar mijn artikel in het vorige nummer voor een voorbeeld assembly waar hiervan gebruik is gemaakt. Voor de routines in Borland.Vcl.IBIntf.pas is hier geen gebruik van gemaakt - daar waren alle globale routines al in een interface of class geplaatst.

Tag Variant
De volgende foutmeldingen gaan over de compatibiliteit tussen een Variant en een Integer. De Tag property was in Delphi onder Win32 altijd van type Integer, maar is in .NET van type Variant geworden (in WinForms is het een type Object overigens). De compiler klaagt erover dat we een Integer in een Variant willen stoppen, en later er weer een Integer uit willen halen. Dat zou toch gewoon mogelijk moeten zijn? En dat is het ook, we moeten alleen even expliciet de Variants unit aan de uses clause toevoegen. Hierna compileert de toepassing van 1591 regels source code zonder problemen. Er blijven nog een dikke 45 warnings over, die allemaal hetzelfde zeggen: unit Borland.Vcl.*** is specifiek voor een bepaald platform. Ja, dat klopt. De VCL for .NET is specifiek bovenop de Win32 API gebouwd, en zal dus niet onder bijvoorbeeld Mono draaien (tenzij daar ook de Win32 API geëmuleerd wordt in de toekomst).

Het compileert, dus het werkt
Dat een toepassing compileert wil nog niet altijd zeggen dat het ook meteen goed werkt. Dat blijkt wel als je de AdminTool in praktijk draait. Het gaat lang goed, totdat je een backup van een InterBase database wilt maken. Dan komt plotseling de volgende foutmelding om de hoek kijken:

Invalid threading model

Dit heeft te maken met het feit dat een aantal van de VCL componenten het STA (Single Threaded Apartment) model vereisen. En dat moeten we bij het begin van de toepassing aangeven met het [STAThread] attribuut. Hiervoor moeten we naar de project source code in AdminTool.dpr, en het attribuut [STAThread] kan vlak voor de eerste begin geplaatst worden.

InterBase Admin Tool

Hierna werkt het wel, en deze keer zonder problemen. We hebben in minder dan een uur een toepassing van 6 forms en 1591 regels code gemigreerd van VCL (voor Win32) naar VCL for .NET.

Cross-platform code
De resulterende toepassing is overigens geen cross-platform source code project geworden: na de aanpassingen compileert het niet meer met Delphi 7 tot een Win32 toepassing. Wie source code wil gebruiken om zowel tot Win32 als .NET executables te komen, zal toch veelvuldig gebruik moeten maken van compiler directives om stukken code voor Win32 van .NET te scheiden. Voor Win32 kunnen we {$IFDEF WIN32} gebruiken, terwijl voor .NET de keuze is uit {$IFDEF CLR}, {$IFDEF MANAGEDCODE} en {$IFDEF CIL}. Let op dat het alternatief voor Win32 niet noodzakelijkerwijs alleen .NET hoeft te betekenen: er is ook nog een {$IFDEF LINUX} voor gebruik in combinatie met Kylix (= Delphi for Linux).

Web Toepassingen?
Dit was misschien een aardig voorbeeld van de migratie van een kleine VCL toepassing naar VCL for .NET, maar hoe zit het met web server toepassingen? Waar we voor GUI toepassingen met Delphi 8 for .NET een keuze hebben tussen WinForms of VCL for .NET (en daar soms goed over na moeten denken), daar is er voor het bouwen van web toepassingen ook een keuze tussen twee alternatieven, namelijk ASP.NET en IntraWeb for .NET. Deze laatste is compatible met de VCL, en kan dus met de BDE, dbExpress of IBX overweg. Waar de VCL for .NET echter door Borland zelf ontwikkeld is, en door third-party component vendors wordt ondersteund, daar is IntraWeb een third-party tool, met een veel kleinere groep gebruikers en beschikbare add-ons. Wie een bestaand IntraWeb project moet migreren naar een safe, managed .NET editie, kan dit met IntraWeb for .NET doen. Het blijven echter ISAPI DLLs of stand-alone toepassingen, en bieden daarmee geen voordeel bovan ASP.NET. Daardoor is het voor de bouw van een nieuwe toepassingen de vraag of de voordelen van compatibiliteit met VCL for .NET opwegen tegen de kracht van ASP.NET.

Samenvatting
Delphi ontwikkelaars die de overstap naar .NET willen maken met Delphi 8 for .NET hebben voor GUI toepassingen de keuze tussen VCL for .NET en WinForms. Migratie van VCL naar WinForms is erg moeizaam, en niet aan te raden. Migratie van VCL naar VCL for .NET is eenvoudiger, alhoewel de inspanning van project tot project zal verschillen. Voor nieuwe toepassingen zal de stap naar WinForms of ASP.NET een leercurve betekenen voor Delphi VCL ontwikkelaard, maar ook een groter potentieel van beschikbare componenten en informatie. Toch kan ook voor nieuwe projecten het gebruik van VCL for .NET te prefereren zijn voor Delphi ontwikkelaars.

De Toekomst: VCL en Longhorn
Windows Longhorn, beschikbaar medio 2006 of 2007, zal WinForms en WebForms ondersteunen, net als Win32. Maar niet langer als de preferred technologie. Een beetje te vergelijken als Windows 3.x toepassingen die nu nog steeds onder XP draaien, maar zonder daarbij gebruik te kunnen maken van alle XP eigenschappen. Wie dus nu in WinForms aan het ontwikkelen gaat, heeft de kans dat straks de WinForms toepassing net zo legacy is als de MFC toepassing van gisteren. Hetzelfde geldt voor VCL (for .NET) toepassingen, die immers bovenop de Win32 API zijn geschreven. Echter, wat betreft de source code van mijn projecten (die gebruikmaken van de VCL en VCL for .NET) hoeft dit niet te betekenen dat ik opnieuw moet beginnen. Het ligt in de lijn der verwachtingen dat er ook een VCL for Longhorn zal komen, die net zo compatible is met VCL als VCL for .NET. En daarmee wederom een migratiepad kan bieden voor bestaande VCL (en VCL for .NET) toepassingen naar Longhorn. Als native Longhorn toepassingen dus. Welke rol XAML hierbij zal spelen - misschien als vervanger of uitbreiding van de huidige .dfm en .nfm forms - dat zal de tijd ons leren. En daar kom ik tegen die tijd vast nog wel eens op terug in dit blad.

Referenties