Bob Swart (aka Dr.Bob)
.NET Assemblies importeren in Delphi 7

C# .NET assemblies importeren en gebruiken met Delphi 7
De afgelopen maanden heb ik enkele artikelen over COM geschreven, met als uiteindelijk doel om bij de combinatie van COM en .NET uit te komen. En nu Delphi 7 zojuist is uitgekomen, ben ik dan ook blij te melden dat we .NET assemblies kunnen importeren en gebruiken in Delphi 7. Niet helemaal automatisch - je moet er nog wel iets voor doen, maar het gaan vrij pijnloos. In dit artikel zal ik laten zien hoe een-en-ander in zijn werk gaat, en aan het eind laat ik zelfs zien dat het ook gevolgen kan hebben voor Delphi 6 gebruikers.

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).
Gelukkig is de type library importer van Delphi 7 wel geschikt gemaakt om .NET type libraries te importeren: met name natuurlijk de mscorlib.tlb die nu een mscorlib_TLB.pas import unit oplevert die wel goed te verwerken is. Net als de Euro42_TLB.pas import unit die uit de Euro42.tlb is gegenereerd. Dus, start Delphi 7, en doe Project | Import Type Library en zoek de Euro42 in de lijst:

Delphi 7 Type Library Importer

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.

Euro42_TLB.TEuro is geïmporteerd en geregistreerd

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.
Het werkt wat leuker als we er twee TEdits (genaamd edtEuro en edtValuta), twee TButtons (genaamd btnToEuro en btnFromEuro) en een TRadioGroup (genaamd Currency) bij zetten. De RadioGroup krijgt 13 strings. De eerste 12 zijn de euro valuta's volgens DING FLOP BIPS, en de 13e is een foute die ik straks wil gebruiken om te testen wat er mis kan gaan (zie het stukje over C# exceptions in Delphi).

Delphi 7 TEuro test programma

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!
Er is nog een andere manier om de .NET assembly te gebruiken, zonder TEuro42 component, namelijk via de Euro42_TLB.pas tpe library import unit, waar een CoEuro42_ class in voorkomt. Deze heeft een class method Create (en CreateRemote voor een remote machine) die een IEuro interface teruggeeft. Met andere woorden: de implementatie van de OnClick event hander van de btnToEuro zonder TEuro component is als volgt:
  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.

Oeps! De Euro42.dll wordt niet gevonden!

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.
Dit betekent helaas wel dat we de source code van de C# .NET assembly een beetje moeten aanpassen, opnieuw moeten compileren, registreren, importeren, etc. We moeten in feit alles opnieuw doen, vrees ik!
Om te beginnen moeten we een AssemblyKeyFile aanmaken in de directory waar de source code van Euro42.cs staat. Dit doen we met de volgende aanroep:
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).
Nu moeten we de Euro42.dll nog deployen in de Global Assembly Cache, zodat we er straks wel zonder problemen bij kunnen (waar de Delphi executable ook staat). Dat doen we als volgt:
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=0ad170caf360281a
Om 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."

Standaard out-of-bounds foutbericht van C#

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).
Hiervoor heb ik de code van FromEuro en ToEuro aan de C# kan als volgt gewijzigd:
  // 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.
Als we nu de "Error" currency kiezen en op een van de knoppen drukken krijgen we een dialoog met daarin de string die we aan de C# kant aan de ApplicationException hebben meegegeven. Leuk om te weten dat de C# exceptions dus aan de COM kant worden omgezet in EOleExceptions met als message een kopie van de message string die in C# werd meegegeven.

Nederlandstalige foutmelding

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.


This webpage © 1999-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.