.NET Assemblies importeren in Delphi 7 |
C# eBob42.Euro42.cs
Om het importeren van .NET assemblies (geschreven in C#) in Delphi 7 te demonstreren hebben we natuurlijk om te beginnen het .NET Framework nodig - al was het maar voor de csc command-line C# compiler (want we maken in dit artikel geen gebruik van Visual C# Standaard of Visual Studio.NET).
Als demo heb ik gekozen voor een IEuro interface met twee methoden: FromEuro en ToEuro.
Beide methoden hebben twee argumenten: het eerste geeft aan welke conversie we willen toepassen (het volgnummer uit een lijst met muntsoorten, volgens de DING FLOF BIPS reeks), en het tweede argument is de floating-point waarde die we willen omzetten.
Het resultaat is eveneens een floating point waarde: het resultaat van de conversie.
Behalve de IEuro definitie, heb ik ook de Euro42 implementatie geschreven.
Maar dan wel met het attribuut ClassInterface(ClassInterfaceType.None) om te zorgen dat er geen class interfaces gegenereerd worden, en overerving slechts via interfaces (de COM manier) kan plaatsvinden.
Op deze manier hebben onze un-managed COM clients (die we in Delphi gaan schrijven) minder versie-problemen als we de .NET assembly later uitbreiden of aanpassen.
using System; using System.Runtime.InteropServices; namespace eBob42 { public interface IEuro { float FromEuro(int Currency, float Amount); float ToEuro(int Currency, float Amount); } // Laat geen class interface genereren - inheritance via interfaces! [ClassInterface(ClassInterfaceType.None)] public class Euro42 : IEuro { private readonly float[] EuroConversionRate = {1.0F, // EURO 1.95583F, // DEM 1936.27F, // ITL 2.20371F, // NLG 340.750F, // GRO 5.94573F, // FIM 40.3399F, // LUF 13.7603F, // ATS 6.55957F, // FRF 40.3399F, // BEF 0.787564F,// IEP 200.482F, // PTE 166.386F}; // ESP // parameterless (default) constructor voor COM interoperability public Euro42 () { } public float FromEuro(int Currency, float Amount) { return Amount * EuroConversionRate[Currency]; } public float ToEuro(int Currency, float Amount) { return Amount / EuroConversionRate[Currency]; } } }Merk op dat we ook een default constructor (zonder argumenten) in onze Euro42 class nodig hebben. Uiteraard dit is maar een eenvoudig voorbeeld, maar de aandacht gaan uit naar de koppeling tussen .NET en COM, en niet zozeer naar de assembly zelf.
Compileren en Registreren
Om nog even bij de .NET kant te blijven: we moeten eerst de code in Euro42.cs compileren en registreren.
Bij het compileren hebben we het /t:library argument nodig om te zorgen dat we een DLL krijgen, en geen executable (dat laatste levert uiteraard de foutmelding op dat we geen entry point hebben).
Dus tik ik het volgende in:
csc /t:library Euro42.cs Microsoft (R) Visual C# .NET Compiler version 7.00.9466 for Microsoft (R) .NET Framework version 1.0.3705 Copyright (C) Microsoft Corporation 2001. All rights reserved. |
Het resultaat is een Euro42.dll assembly die nu geregistreerd moet worden met regasm (waardoor er een proxy object gemaakt wordt - ook wel de COM Callable Wrapper genoemd). Door het /tlb argument mee te geven wordt een type library gegenereerd en mee geregistreerd. Regasm is wat dat betreft een beetje te vergelijken met tregsvr van Borland: het maakt de Windows registry keys aan die het IEuro interface beschikbaar maakt voor unmanaged COM clients. Let op dat deze type library iedere keer opnieuw wordt aangemaakt als je regasm aanroept (wat je dus eigenlijk maar één keer hoeft te doen - tenzij de signature wijzigt, wat we later zullen zien).
regasm /tlb Euro42.dll Microsoft (R) .NET Framework Assembly Registration Utility 1.0.3705.288 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Types registered successfully Assembly exported to 'D:\src\Euro42.tlb', and the type library was registered successfully |
Nu kunnen we de Euro42.tlb importeren en gebruiken in Delphi 7.
Importeren en Gebruiken
Maar wacht eens even, zullen Delphi 6 gebruikers zeggen: als de Euro42.tlb type library gegenereerd en ook al geregistreerd is door regasm, dan kunnen we die toch ook in Delphi 6 al gebruiken? Bijna.
Helaas levert dit nog de nodige foutmeldingen op, doordat het importeren van een .NET type library als bijeffect heeft dat de mscorlib.tlb ook geïmporteerd moet worden (de mscorlib assembly bevat de basis van het .NET Framework).
Overigens wordt de mscorlib.tlb al door de Delphi 7 installer met regasm geregistreerd, dus hoeven we dat niet nog eens met de hand te doen (wat wel moet als je alleen Delphi 6 gebruikt).
En de resulterende mscorlib_TLB.pas import unit van zo'n anderhalve meg geeft helaas de nodige foutmeldingen als we die in Delphi 6 proberen te compileren.
Wat wel in Delphi 6 kan is om via Variants gebruik te maken van het Euro42.IEuro interface.
Dat kan bijvoorbeeld met de volgende code:
var
Euro: Variant;
begin
Euro := CreateOLEObject('eBob42.Euro42'); // NameSpace.ClassName
ShowMessage(FloatToStr(Euro.FromEuro(3,100)));
Helaas is dat code die van late-binding gebruik maakt.
Noodzakelijk, omdat we in dit geval dus niet de type library kunnen importeren in Delphi 6 (of liever gezegd: de import unit compileert niet).
Klik op Install... om een import unit Euro42_TLB.pas te genereren (die komt in de Delphi7\Imports directory, net als de mscorlib_TLB.pas). Na installatie in de dclusr70.bpl krijgen we de bevestiging dat de TEuro42 component geregistreerd is.
We kunnen nu op twee manieren "contact" maken tussen code in Delphi 7 en de Euro42 assembly onder .NET. De makkelijkste manier is op de TEuro42 component van de ActiveX tab van het component palette op een form te zetten, en daarvan de methoden FromEuro of ToEuro aan te roepen.
De implementatie van de OnClick event handler van btnFromEuro maakt gebruik van de TEuro42 component en is als volgt (eigenlijk maar één regel code):
procedure TForm1.btnFromEuroClick(Sender: TObject); begin edtValuta.Text := FloatToStr( Euro42.FromEuro(Succ(Currency.ItemIndex), StrToFloatDef(edtEuro.Text,0))) end;Niks bijzonders dus eigenlijk. Ware het niet dat we nu vanuit onze non-managed Delphi 7 code een aanroep doen naar een managed .NET assembly!
procedure TForm1.btnToEuroClick(Sender: TObject); var Euro: IEuro; begin Euro := CoEuro42_.Create; edtEuro.Text := FloatToStr( Euro.ToEuro(Succ(Currency.ItemIndex), StrToFloatDef(edtValuta.Text,0))) end;Ook hier zou je weer een (lange) one-liner van kunnen maken uiteraard:
procedure TForm1.btnToEuroClick(Sender: TObject); begin edtEuro.Text := FloatToStr( CoEuro42_.Create.ToEuro(Succ(Currency.ItemIndex), StrToFloatDef(edtValuta.Text,0))) end;Als je nu het project compileert met Delphi 7 zul je zien dat je inderdaad Euros kunt converteren naar andere valutas, en dat Delphi 7 daarbij gebruik maakt van een .NET assembly geschrreven in C#.
Global Assembly Cache
Wie tot nu heeft meegedaan heeft een kans dat het niet werkt.
Je kan een OLE error 80131522 die aangeeft dat de library niet gevonden kan worden.
Dit wordt veroorzaakt als de Euro42.dll niet in dezelfde directory staat als het Delphi project. Omdat dat niet altijd een even fijne manier van deployen is (ik kan me voorstellen dat je de .NET assembly wil delen met anderen, en dan is het niet handig om hem overal neer te zetten), kunnen we beter gebruik maken van de Global Assembly Cache die Microsoft heeft uitgevonden om assemblies te deployen zodat ze door clients gebruikt kunnen worden.
sn -k eBob42.snk Microsoft (R) .NET Framework Strong Name Utility Version 1.0.3705.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Key pair written to eBob42.snk |
Nu moeten we het AssemblyKeyFile attribuut toevoegen aan onze source file, en daarvoor de System.Reflection; aan de using clause toevoegen. De code Euro42.cs wordt als volgt aangepast (de inhoud van de namespace eBob42 veranderd niet, dus die heb ik niet meer opnieuw opgenomen):
using System; using System.Runtime.InteropServices; using System.Reflection; // GAC [assembly:AssemblyKeyFile("eBob42.snk")] // GAC namespace eBob42 { ... }We moeten opnieuw de Euro42.cs compileren. Dit levert een Euro42.dll die vervolgend helaas niet meer te gebruiken is met het Delphi project (als het hiervoor wel werkte, krijgen we nu in ieder geval de eerder vermelde OLE error). Als we de Euro42 straks opnieuw importeren in Delphi 7 zien we dat de GUIDs zijn veranderd, waardoor de oude type library niet langer overeenkomt met wat er in de nieuwe Euro42.dll zit. Kortom: we moeten ook de type library opnieuw genereren met regasm /tlib Euro42.dll (dat hadden we al eerder gedaan).
gacutil -i Euro42.dll Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.3705.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Assembly successfully added to the cache |
In de directory C:\WinNT\assembly\GAC is nu een subdirectory Euro42 aangemaakt, met daarin een subdirectory met de naam 0.0.0.0__0ad170caf360281a (mijn public key token - deze naam zal uniek zijn voor mijn systeem). In deze subdirectory staat nu de Euro42.dll net als een __AssemblyInfo__.ini met de volgende inhoud:
[AssemblyInfo] MVID=95374403e3e08d498a21e8ec69b1d1e8 URL=file:///D:/src/Euro42.dll DisplayName=Euro42, Version=0.0.0.0, Culture=neutral, PublicKeyToken=0ad170caf360281aOm de Euro42.dll weer uit de Global Assembly Cache te krijgen moeten we gacutil met de -u flag aanroepen. Let erop dat we dan Euro42 zonder de .dll extensie erbij moeten opgeven:
gacutil -u Euro42 Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.3705.0 Copyright (C) Microsoft Corporation 1998-2001. All rights reserved. Assembly: Euro42, Version=0.0.0.0, Culture=neutral, PublicKeyToken=bcb56a2022794384, Custom=null Uninstalled: Euro42, Version=0.0.0.0, Culture=neutral, PublicKeyToken=bcb56a2022794384, Custom=null Number of items uninstalled = 1 Number of failures = 0 |
Maar uiteraard willen we de Euro42.dll eerst nog even een paar keer testen met ons Delphi project, dus laat hem nog maar even in de Global Assembly Cache staan.
Nu kunnen we in ieder geval de Delphi executable op een willekeurige plek op onze machine zetten en uitvoeren: de Euro42.dll hoeft niet langer in dezelfde directory te staan.
En converteren gaat nog steeds goed.
Tijd om de "Error" currency te proberen, en te zien hoe gecombineerde foutafhandeling plaatsvindt.
C# Exceptions
Als we in de Currency RadioGroup de Error currency selecteren en dan op een van de knoppen drukken, zal er een aanroep van FromEuro of ToEuro plaatsvinden met de waarde 13 als eerste argument.
Omdat aan de C# kant het EuroConversionRate array maar van 0 tot 12 loopt, gaat dat dus fout.
Heel vroeger zou dat boem betekenen, maar tegenwoordig krijg je daar een nette foutmelding voor, en loopt alles gewoon door (behalve dan de conversie, want die kon niet uitgevoerd worden natuurlijk).
De foutmelding die we in dit geval krijgen is "Index was outside the bounds of the array."
De vraag die ik mezelf hierbij stelde is of ik deze exception nog kan beïnvloeden vanaf de C# kant (dus bijvoorbeeld om een nederlandstalige foutmelding te geven).
// Implement MyInterface methods
public float FromEuro(int Currency, float Amount) {
if ((Currency < 0) || (Currency > 12))
throw new ApplicationException("Ongeldige Currency");
return Amount * EuroConversionRate[Currency];
}
public float ToEuro(int Currency, float Amount) {
if ((Currency < 0) || (Currency > 12))
throw new ApplicationException("Ongeldige Currency");
return Amount / EuroConversionRate[Currency];
}
C# fans willen wellicht hun eigen exception type afleiden en gebruiken, maar voor deze demo is de ApplicationExcetopion voldoende.
Natuurlijk kun je aan de Delphi kant ook een try-except gebruiken om de C# exception netjes af te vangen. Het type van de exception is altijd EOleException (ongeacht wat er aan de C# kant wordt gebruikt).
Meer Delphi en .NET
Tot zover Delphi 7 en het gebruik van .NET assemblies in Delphi 7.
In dit artikel hebben we gezien hoe we in C# een interface en class implementatie kunnen schrijven, hoe we die compileren en registreren (en in de Global Assembly Cache stoppen), en vervolgens importeren en kunnen gebruiken op twee manieren in Delphi 7.
Als laatste tip wil ik nog verklappen dat de door Delphi 7 gegenereerde import units mscorlib_TLB.pas en Euro42_TLB.pas ook zonder problemen door Delphi 6 te compileren zijn (met andere woorden: Delphi 6 kan ze niet foutloos genereren, maar wel compileren).
Het voorbeeld programma dat ik in dit artikel met Delphi 7 heb geschreven kunnen we dan ook zonder problemen in Delphi 6 compileren, mits we daarbij de mscorlib_TLB.pas en Euro42_TLB.pas gebruiken die door Delphi 7 eerder gegenereerd werden.
Meer Informatie
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .
Wie meer wil zien betreffende Delphi en de samenwerking met .NET, zou zeker een bezoek aan mijn Delphi en .NET Clinic kunnen overwegen.