Delphi for .NET Assemblies, Units, Namespaces en AppDomains
Dit artikel bevat informatie die ik ook tijdens mijn sessie bij de Software Developer Event op 24 maart heb gepresenteerd. Ik laat in dit artikel zien hoe we Delphi for .NET assemblies kunnen maken en gebruiken, en welke rol units spelen bij het bepalen van de namespace voor de inhoud van de units. Ook laat ik zien hoe we .NET assemblies dynamisch kunnen laden, en beschrijf ik een techniek om .NET assemblies ook weer te kunnen verwijderen (unloaden) door gebruik te maken van AppDomains.
.NET Assemblies
Als we de Delphi for .NET preview command-line compiler die bij Delphi 7 zat even buiten beschouwing laten, zijn er op dit moment drie verschillende versies van Delphi for .NET beschikbaar: Delphi 8 for .NET, Delphi 2005 en Delphi 2006. Wie Delphi 8 for .NET gebruikt kan ik aanraden om over te stappen – liefst naar Delphi 2006. De redenen hiervoor zullen o.a. in dit artikel naar voren komen.
.NET Assemblies kun je aan de ene kant zien als .NET DLLs, de tegenhangers van Win32 DLLs. Maar waar een Win32 DLL alleen maar “platte” functies exporteert, daar bevat een .NET Assembly juist classes met methods. En juist geen “globale” routines of data.
Om een .NET Assembly te bouwen in Delphi for .NET moet je na File | New – Other in de Object Repository naar de Delphi for .NET Projects categorie gaan. Daar zie je de aloude Library staan, alsmede de Package.

Fig. 1: Object Repository
Alhoewel ik net zei dat een .NET Assembly de .NET tegenhanger is van een Win32 DLL, moeten we hier toch niet kiezen voor een Library, maar juist voor een Package project.
.NET Assemblies zijn net Delphi Packages
De Library kan gebruikt worden voor een unsafe assembly, die ook vanuit Win32 nog aan te roepen is. Dat is leuk voor Interop, maar niet het onderwerp van vandaag. Kies daarom voor een Package project.
Dit levert een Package1 project op, met daarin een Contains lijst (waar onze eigen units in komen) en een Requires lijst, met daarin al meteen de Borland.Delphi.dll (waar de Delphi for .NET RTL in zit – daar komen we zo nog op terug).

Fig. 2: Project Manager
Ik bewaar de package als project met de naam eBob42.NET, waardoor het uiteindelijk de .NET Assembly eBob42.NET.dll zal opleveren.
Units en Namespaces
Tijd om een unit toe te voegen aan de Contains lijst. De naam die we aan de unit geven is belangrijker dan die in de Win32 wereld was. In de Delphi for .NET wereld is de naam van de unit namelijk bepalend voor de namespace van de elementen die in de unit gedefinieerd zijn. En de namespace op zijn beurt kun je zien als een logische container of naam die elementen groepeert, en er voor zorgt dat we elementen met eenzelfde naam toch op meerdere plaatsen kunnen definiëren (zolang de combinatie namespace met naam maar uniek is).
In de Win32 wereld bepaalt de naam van de unit de namespace. Datzelfde geldt helaas ook voor Delphi 8 for .NET. En dat is jammer, want in de .NET wereld zijn namespaces ook zichtbaar voor de buitenwereld (voor wie een .NET Assembly gebruikt bijvoorbeeld). En dan is het fijn om de namespaces te kunnen gebruiken als logische indeling van je toepassing, in plaats van de namespaces die meteen de fysieke indeling van je toepassing laten zien. Zeker als je als richtlijn iedere class in zijn eigen unit stopt, is het onzinnig om te zien dat als gevolg daarvan in Delphi 8 for .NET iedere class ook in zijn eigen namespace zit.
Delphi 2005 en Delphi 2006 doen het wat dat betreft wel wat beter. Hier is de unitnaam nog wel bepalend voor de namespace, maar niet de gehele unitnaam. Het laatste deel, vlak voor de .pas, wordt weggelaten. Dus de unit eBob42.Test.pas heeft dezelfde namespace als een unit eBob42.Lening.pas, namelijk eBob42. Hierdoor hebben classes in beide verschillende units toch dezelfde logische namespace. En zo hoort het ook.
Het betekent wel dat als er geen punten in de unitnaam zitten (zoals Unit1.pas), dat dan de unitnaam weer wel de hele namespace definieert. Tenzij er in het project een default namespace is gedefnieerd, dan wordt dat namelijk de namespace van de unit-zonder-punten-in-de-naam.
De default namespace van een project kun je overigens instellen in de Directories/Conditionals pagina van de Project | Options dialoog (wellicht niet de meest logische plek):

Fig. 3: Project Options
Deployment
Als we eenmaal een of meerdere units met inhoud aan onze .NET Assembly hebben toegevoegd, is het tijd om de .NET Assembly te deployen. Dat kan op twee manieren: signed in de GAC, of niet signed, en dan naast de executable die de .NET Assembly gebruikt. Het .NET Framework maakt namelijk niet langer meer gebruik van zoekpaden om naar .NET Assemblies te zoeken, waar in Win32 wel het hele pad wordt gebruikt om naar DLLs te zoeken. Er wordt maar in twee plaatsen gekeken: in de huidige directory, en in de Global Assembly Cache (GAC).
Deployment in de huidige directory (lees: dezelfde directory als de executable die de .NET Assembly gebruikt) is natuurlijk het makkelijkst, maar dan wordt er geen hergebruik toegepast, en dat is zonde als de .NET Assembly door meerdere toepassingen gebruikt wordt. Ook kost het meer moeite om de .NET Assembly te updaten (op de deployment machines) als er nieuwere versies uitkomen.
De Global Assembly Cache biedt wel die mogelijkheden, maar daartoe moet we een .NET Assembly eerst signen met een strong name. Het is namelijk niet toegestaan om een “niet veilige” .NET Assembly in de GAC op te nemen: dan zouden virussen vat kunnen krijgen om de .NET Assembly (omdat er geen checksum is die garandeert dat er niet met de code van de .NET Assembly is geknoeid).
In beide gevallen moet de afhankelijke .NET Assemblies ook worden mee-gedeployed. In ons voorbeeld is dat in ieder geval de Borland.Delphi.dll (te zien in figuur 2).
Globale Routines en Data?
Hoe zit het dan met globale routines en data die we bij Delphi/Win32 gewend waren in DLLs te stoppen?
Eerder in dit artikel gaf ik al aan dat .NET Assemblies met name geschikt zijn voor het exporteren van classes, en daar zijn ook genoeg voorbeelden van te vinden. Maar hoe zit het dan met globale routines en data die we bij Delphi/Win32 gewend waren in DLLs te stoppen? Alhoewel Delphi for .NET nog steeds globale functies en procedures kan declareren, kan een taal als C# daar niks mee (puur omdat daar geen globale routines mogelijk zijn).
Om te voorkomen dat een Delphi for .NET assembly niet bruikbaar zou zijn door C#, is door de makers van Delphi een “list” bedacht, waardoor globale routines stiekem toch in een class gestopt worden.
Dit valt het best te demonstreren aan de hand van een voorbeeld. Stel ik wil een functie bouwen die op basis van een geldbedrag en een hoeveelheid afbetalingen, de hoogte van de aflossing berekent. Een eenvoudige implementatie – zonder rente te heffen – zou als volgt zijn:
unit eBob42.Lening;
interface
function Afbetaling(Bedrag: Double;
Termijnen: Integer): Double;
implementation
function Afbetaling(Bedrag: Double;
Termijnen: Integer): Double;
begin
Result := Bedrag / Termijnen
end;
end.
Listing 1: Globale Functie Afbetaling
De daadwerkelijke implementatie zal uiteraard anders zijn als er rente wordt berekend, maar daar gaat het nu nog niet om (dat komt straks).
Als we de .NET Assembly eBob42.NET met daarin de unit eBob42.Lening compileren, dan kunnen we deze assembly toch gebruiken in een C# omgeving, en we kunnen zelfs de routine Afbetaling aanroepen. Alleen is het geen globale functie Afbetaling, maar een functie die onderdeel is van de class eBob42.Unit.
Reflection
Met Borland’s eigen Reflection tool, te vinden in het Tools menu, kunnen we de eBob42.NET.dll assembly openen, en dan zien dat er een namespace eBob42.Units in zit, met daarbinnen een class Lening (het laatste deel van de unit naam eBob42.Lening) die de functie Afbetaling bevat.

Fig. 4: Borland Reflection
Het feit dat er voor de buitenwereld een class genaamd Lening bestaat, hadden we ook zelf expliciet tot uitdrukking kunnen brengen door de eBob42.Lening unit als volgt te schrijven:
unit eBob42.Lening;
interface
type
Lening = class
class function Afbetaling(Bedrag: Double;
Termijnen: Integer): Double; static;
end;
implementation
class function Lening.Afbetaling(Bedrag: Double;
Termijnen: Integer): Double;
begin
Result := Bedrag / Termijnen
end;
end.
Listing 2: Class Functie Afbetaling
Het enige verschil is dat de namespace van de class Lening nu eBob42 is geworden, in plaats van de eBob42.Units die we kregen bij echte globale routines. Los van de verschillende namespace is het gedrag verder hetzelfde.
Gebruik binnen C#
Om dit te demonstreren kunnen we een C# ontwikkelomgeving starten (zoals VS.NET of BDS zelf). Ik gebruik Delphi 2005 om een C#Builder project te starten. Via de Add Reference dialoog kunnen we dan de eBob42.NET.dll assembly toevoegen aan het project.

Fig. 5: Add Reference
De namespace van de unit eBob42.Lening is eBob42, dus moeten we in C# de regel
using eBob42;
toevoegen in onze WinForm.cs unit. Die namespace levert echter alleen de classes op die binnen de unit zijn gedefinieerd. Dat kan de Lening class zijn, als we de function Afbetaling als class method hebben toegevoegd.
Als we function Afbetaling nog steeds als globale routine hebben staan, en dus bij de globale routines en data moeten zien te komen, dan moeten we in plaats van de namespace eBob42 de namespace eBob42.Units toevoegen:
using eBob42.Units;
Daarna moeten we de class Lening gebruiken, en daarvan de member functie Afbetaling aanroepen, wat als volgt kan:
Lening.Afbetaling(1000,24);
Merk op dat we geen instantie van de class Lening hoeven te maken (het is een globale class, net als de MessageBox class bijvoorbeeld).
Over MessageBox gesproken, het resultaat kunnen we als volgt vertonen:
MessageBox.Show("1200 Euro in 24 termijnen = " +
Convert.ToString(Lening.Afbetaling(1200, 24)));
Het resultaat van deze aanroep is als volgt (waarmee tevens het bewijs is geleverd dat de C# executable de Delphi for .NET assembly daadwerkelijk gebruikt):

Fig. 6: Resultaat
Gebruik binnen Delphi for .NET
Indien we de Delphi for .NET assembly in Delphi zelf willen gebruiken, hebben we natuurlijk niet langer te maken met namespaces, maar juist met unit namen in de uses clause.
Er is echter nog een ander potentieel probleem dat Delphi for .NET gebruikers te wachten staat. Zoals in figuur 2 al te zien was, is de eBob42.NET.dll afhankelijk van de Borland.Delphi.dll assembly. En dat betekent dat deze .NET assembly meegeleverd moet worden met de eBob42.NET.dll assembly, anders zal hij het niet doen, tenzij hij toevallig op een machine komt waar de juiste versie van de Borland.Delphi.dll assembly al staat. Het keyword hier is “de juiste versie”. Er zijn namelijk voor het .NET Framework versie 1.1 al drie verschillende versies van de Borland.Delphi.dll: die van Delphi 8 for .NET, die van Delphi 2005 en die van Delphi 2006 … en ze zijn helaas niet uitwisselbaar. Je voelt de bui wellicht al hangen: als ik de eBob42.NET.dll assembly gecompileerd heb met Delphi 2006, dan is hij bruikbaar in vrijwel iedere .NET ontwikkelomgeving (inclusief C#), behalve Delphi 8 for .NET en Delphi 2005. Want voor de laatste twee ontwikkelomgevingen geldt dat zij niet overweg kunnen we de Borland.Delphi.dll uit Delphi 2006 (en vice versa), waardoor de eBob42.NET.dll niet gebruikt kan worden binnen die omgevingen.
Delphi Component Bouwers zullen dan ook drie verschillende binary versies van hun assemblies moeten leveren (of gewoon de source code na aanschaf, zodat de gebruiker zelf de .NET assembly opnieuw kan compileren indien nodig).
Ik hoop dat Delphi Highlander voor .NET 2.0 deze afhankelijkheid van de Borland.Delphi.dll kan oplossen, zodat we straks niet weer in problemen komen als er meerdere versies van de Borland.Delphi.dll voor het .NET Framework 2.0 uitkomen.
.NET Assemblies en AppDomains
Tijd om naar het volgende onderwerp over te stappen: .NET Assemblies en AppDomains. Wat AppDomains zijn en hoe we ze kunnen gebruiken volgt zo, eerst zal ik de reden van het gebruik van AppDomains demonstreren.
Het .NET Framework biedt de mogelijkheid om een .NET Assembly dynamisch te laden vanuit een executable door de Assembly.LoadFrom method aan te roepen, en daarbij de filename van de te laden .NET Assembly mee te geven. Om onze eBob42.NET.dll assembly te laden is dat dus:
Assembly.LoadFrom(‘eBob42.NET.dll’);
We gaan er dus va uit dat de aanroepende toepassing de eBob42.NET.dll assembly kan vinden (in de huidige directory of – indien gesigned – in de GAC).
Er is echter geen manier om een .NET Assembly weer uit het geheugen te verwijderen. Eenmaal geladen blijft de assembly geladen tot we de gehele toepassing afsluiten. Daar valt wellicht best iets voor te zeggen, maar is toch niet altijd de meest welkome manier om dynamisch gebruik te maken van .NET Assemblies. Je wilt ook kunnen switchen, zonder de gehele toepassing af te sluiten. Stel bijvoorbeeld dat er een aantal externe .NET Assemblies zijn die allemaal een class Lening exporteren met een class method Afbetaling, waarbij de uitkomst steeds bepaald wordt door de interne rentestand van die partij. Dan zou het voor een tussenpersoon makkelijk en handig zijn om dynamisch een verzameling .NET Assemblies te kunnen laden (en weer te verwijderen).
Een AppDomain is een geïsoleerd proces dat in zijn eigen wereld leeft, en zijn eigen security settings kan hebben.
Neem nu een AppDomain, voluit Application Domain genoemd. Een AppDomain is een geïsoleerd proces dat in zijn eigen wereld leeft, en zijn eigen security settings kan hebben. Iedere .NET toepassing draait in een AppDomain. Door nu een nieuw AppDomain te maken, hebben we de mogelijkheid om een .NET Assembly in dat AppDomain te laden en dan via .NET Remoting met die assembly te communiceren. Als we de .NET Assembly niet meer nodig hebben, maar deze assembly niet zelf kunnen unloaden, kunnen we nu wel het bijbehorende AppDomain unloaden (waardoor de betreffende .NET Assembly wel uit het geheugen verwijderd zal worden).
Wrapper Assembly
De beste manier om dit te demonstreren is het bouwen van een wrapper assembly die door .NET executables gebruikt kan worden, en die waar nodig een AppDomain aanmaakt, daar de eBob42.NET.dll assembly in laadt, de functionaliteit daarin vervolgens beschikbaar stelt aan de aanroepende toepassing, en wanneer gewenst het AppDomain (inclusief de eBob42.NET.dll assembly) weer uit de lucht kan halen.
Start Delphi for .NET, en maak een nieuwe package die de naam HostASM krijgt. Voeg een unit toe met de naam Host en de volgende inhoud:
unit Host;
interface
uses
System.Reflection;
type
HostControl = class(MarshalByRefObject)
private
LeningASM: Assembly;
public
procedure LoadLeningASM(const Name: String);
function Afbetaling(Bedrag: Double;
Termijnen: Integer): Double;
end;
implementation
{ HostControl }
procedure HostControl.LoadLeningASM(const Name: String);
begin
LeningASM := Assembly.LoadFrom(FileName)
end;
function HostControl.Afbetaling(Bedrag: Double;
Termijnen: Integer): Double;
var
T: &Type;
M: MethodInfo;
O: System.Object;
begin
if Assigned(LeningASM) then
begin
T := LeningASM.GetType('eBob42.Lening');
O := Activator.CreateInstance(T);
M := T.GetMethod('Afbetaling');
Result := Convert.ToDouble(
M.Invoke(O, [Bedrag, Termijnen]))
end
else Result := 0
end;
end.
Listing 3: Assembly Wrapper
Deze wrapper biedt de mogelijkheid om de .NET Assembly meegegeven als argument aan de LoadLeningASM te laden, en de Afbetaling functie aan te roepen. Op zich lost dit nog niks op, maar door deze wrapper aan een AppDomain te hangen kunnen we hem wel uit de lucht halen wanneer nodig.
Om dit te demonstreren is een simpele Delphi for .NET console toepassing voldoende, met de volgende inhoud:
program Leningen;
{$APPTYPE CONSOLE}
{%DelphiDotNetAssemblyCompiler 'HostASM.dll'}
uses
SysUtils,
System.Reflection,
Host;
function CreateAndUseAppDomain(const Name: String;
Bedrag: Double; Termijnen: Integer): Double;
var
A,D: AppDomain;
H: HostControl;
begin
A := AppDomain.CreateDomain('MyNewAppDomain');
H := A.CreateInstanceAndUnwrap('HostASM',
'Host.HostControl') as HostControl;
H.LoadLeningASM('eBob42.NET.dll');
Result := H.Afbetaling(1200, 24);
H.Free;
System.AppDomain.UnLoad(A)
end;
begin
writeln('eBob42.NET.dll: 1200 Euro in 24 maanden: ',
CreateAndUseAppDomain('eBob42.NET.dll',1200,24):1:2);
writeln('AppDomain is weg');
readln;
writeln('eBob42.NET.dll: 2400 Euro in 12 maanden: ',
CreateAndUseAppDomain('eBob42.NET.dll',2400,12):1:2);
writeln('AppDomain is weg');
end.
Listing 4: Console Toepassing
Listing 4 laat zien hoe we de CreateAndUseAppDomain functie kunnen gebruiken om een nieuwe AppDomain te maken, een .NET Assembly daarbinnen kunnen laden (onze HostASM.dll) en deze kunnen gebruiken om meerdere assemblies te laden, om uiteindelijk de gehele AppDomain weer via Unload te verwijderen. Als je in de Host unit (of de eBob42.Lening unit) in de initialization en finalization wat debugging code zet, dan zul je zien dat de .NET Assemblies inderdaad dynamisch geladen en weer verwijderd worden op commando.
Conclusies
In dit artikel heb ik laten zien hoe we .NET Assemblies kunnen bouwen met Delphi for .NET, en hoe we die, behalve in andere versies van Delphi for .NET, ook kunnen gebruiken in een .NET ontwikkelomgeving. Ik heb laten zien hoe units en namespaces samenhangen, en waar globale routines blijven. Tot slot heb ik laten zien hoe we met behulp van AppDomains .NET Assemblies dynamisch kunnen laden, en samen met het hele AppDomain weer kunnen verwijderen.
Referenties