Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics Dr.Bob's Delphi Courseware Manuals
 Dr.Bob Examines... #36
See Also: other Dr.Bob Examines columns or Delphi articles

An earlier version of this article originally appeared in Delphi Developer (October 2002). Copyright Pinnacle Publishing, Inc. All rights reserved.

Delphi, COM and .NET
This is the first of a number of articles about Delphi and .NET, starting off with a demonstration how to use .NET assemblies in "old" Win32 applications by importing them as COM Objects using Delphi 7 Studio.

For this article, you need the .NET Framework and SDK installed (but you do not need Visual Studio.NET or even C# standard, since we're using the .NET command-line tools only). You also need a copy of Delphi 7 in order to successfully import and use the .NET assemblies.

C# eBob42.Euro42.cs
In order to import a .NET assembly, we first need to find one. In order to show all the steps from start to end, I decided to write one from scratch using C#. If only to illustrate the use of the csc C# command-line compiler. For this demo, I again decided to take the Euro example, consisting of an IEuro interface with two methods: FromEuro and ToEuro. Both methods have two arguments: the first one an integer to determine the currency we want to convert from (or two), and the second one a floating point currency value. The result is also a floating-point currency value.
Apart from the IEuro interface, I've also build the Euro42 class implementation in C#. This class had to be declared with the attribute ClassInterface(ClassInterfaceType.None) to avoid the generation of class interfaces, and enforce inheritance using interfaces only (the COM way). This way, the unmanaged COM clients written in Delphi will encounter less versioning problems if the .NET assembly is later changed or updated. Anyway, the C# source code for the .NET assembly that we will import and use, can be seen in Listing 1.

  using System;
  using System.Runtime.InteropServices;

  namespace eBob42 {
    public interface IEuro {
      float FromEuro(int Currency, float Amount);
      float ToEuro(int Currency, float Amount);
    }

    // Don't generate class interface - inheritance using 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
      public Euro42 () {
      }

      public float FromEuro(int Currency, float Amount) {
        return Amount * EuroConversionRate[Currency];
      }
      public float ToEuro(int Currency, float Amount) {
        return Amount / EuroConversionRate[Currency];
      }
    }
  }

Compilations and Registration
We must now compile the Euro42.cs source file, using the /t:library flag in order to produce a DLL instead of an executable (there's no entry point, so we wouldn't get an executable anyway). The following is a transcript of what you have to type to a .NET command-line, as well as the response back:

  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.
The result is a Euro42.dll assembly that has to be registered using regasm in order to produce a proxy object (also known as a COM Callable Wrapper) with which the unmanaged COM client can communicate. By using the /tlb argument, we can force regasm to produce and register a type library for the Euro42.dll assembly:
  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 'Euro42.tlb', and the type library was registered successfully
At this time, we have a Euro42.tlb type library, which can be imported and used in Delphi.

Importing and Using
Those of you who have not yet upgraded to Delphi 7 may wonder why we can't use Delphi 6 to import the type library. Unfortunately, that is caused by the fact that any .NET assembly that is imported will automatically force the mscorlib.tlb (the .NET Framework core type library) to be imported as well. Note that if you do not have a mscorlib.tlb on your machine, you must create it using regasm as we've just done with Euro42.dll (also note that the Delphi 7 installer will automatically generate and register the mscorlib.tlb for you - in case you install Delphi 7 on a machine with the .NET Framework installed, that is).
The problem with Delphi 6 is caused by the fact that the import unit mscorlib_TLB.pas generated for mscorlib.tlb doesn't compile correctly. And this is a show-stopper for any .NET assembly import attempts using Delphi 6. Fortunately, Delphi 7 fixes this problem, and the mscorlib_TLB.pas import unit generated by Delphi 7 compiles without problems (it even compiles using Delphi 6, although there is no reason to "step back" to Delphi 6 if you already have Delphi 7 on your machine).
For those of you who are still using Delphi 6, there's only one solution: using Variants (as we saw earlier in this Delphi COM series). Even without importing a type library, you can still use it as long as you know the right name of the object to invoke. In our case, that's eBob42.Euro42 (the namespace eBob42 followed by the name of the class that implements the IEuro interface). So in Delphi 6 or later, we can always write the following code to use the .NET assembly:

  var
    Euro: Variant;
  begin
    Euro := CreateOLEObject('eBob42.Euro42'); // NameSpace.ClassName
    ShowMessage(FloatToStr(Euro.FromEuro(3,100)));
Although this illustrates how we can use the proxy class in the Euro42.tlb to access the Euro42.dll .NET assembly, we are only using late-binding. So no compile time checking or Code Insight support for methods and arguments.

Importing with Delphi 7
The type library importer of Delphi 7 is capable of producing compilable import units for .NET assemblies (including the mscorlib.tlb beast). In order to demonstrate this, start Delphi 7, do Project | Import Type Library, and search for the Euro42.tlb in the list of registered type libraries.

Delphi 7 Type Library Importer

Click on the Install... button to generate an import unit Euro42_TLB.pas and - since we also checked the "Generate Component Wrapper" option to install it on the Component Palette. If everything compiles without problems, then we'll get a confirmation that the Euro42_TLB.TEuro42 component is installed in the (default) dclusr70.bpl package. Again, if you try to do this with Delphi 6, it will fail somewhere in the mscorlib_TLB.pas import unit.

Euro42_TLB.TEuro42 registered

Apart from the Variants way, there are now two more ways to connect our Delphi 7 code with the Euro42 .NET assembly. The easiest way is to use the TEuro42 component which should now be found on the ActiveX tab of the Delphi 7 Component Palette. Just drop this component, and we can call the FromEuro or ToEuro methods. In order to demonstrate this, I've build a little demo application in Delphi 7 again, using 2 TEdits (called edtEuro and edtValuta), two TButtons (called btnFromEuro and btnToEuro) and a TRadioGroup named Currency, which has 12 European currencies abbreviations, as well as an Error entry (the 13th option - with ItemIndex equal to 13). We'll use this last option to investigate error handling and recovery between .NET assemblies and Delphi.

Delphi 7 TEuro test application

For now, the implementation of the OnClick event handler of the btnFromEuro button uses the simple TEuro42 component, and is as follows (in fact just one long line of code):

  procedure TForm1.btnFromEuroClick(Sender: TObject);
  begin
    edtValuta.Text := FloatToStr(
      Euro42.FromEuro(Succ(Currency.ItemIndex),
        StrToFloatDef(edtEuro.Text,0)))
  end;
This is nothing special, until you realise that from an unmanaged Delphi 7 application, we are calling a proxy object (the COM Callable Wrapper) that in turn connects our call to the managed .NET assembly!
Apart from using the TEuro42 component, we can also decide to only use the type library import unit Euro42_TLB.pas in case you don't want or need to install the resulting object as component on the component palette. In the type library import unit, we can find a class CoEuro42_ with a class method Create producing an IEuro result (as well as a CreateRemote to create an instance from a remote machine). Using the CoEuro42_ class instead of the TEuro42 component to produce the IEuro interface, we can implement the OnClick event handler of the btnToEuro button as follows:
  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;
If you want to, you can even turn this into a (very) long one-liner, of course:
  procedure TForm1.btnToEuroClick(Sender: TObject);
  begin
    edtEuro.Text := FloatToStr(CoEuro42_.Create.ToEuro(Succ(Currency.ItemIndex),
      StrToFloatDef(edtValuta.Text,0)))
  end;
When compiled with Delphi 7, both event handlers will work just fine in our example, connecting to the proxy object on top of the managed C# .NET assembly, converting Euros to other currencies and back.

Global Assembly Cache
Those of you with Delphi 7 (and the .NET Framework and SDK installed) may have seen that it worked just fine indeed. Or you have just received an OLE error 80131522, meaning that the requested object cannot be located.

Euro42.dll cannot be found

This error is caused if the Euro42.dll is not placed in the exact same directory where our Delphi 7 executable is residing. Even if Euro42.dll is located in the search path or registered, the Delphi 7 executable using the type library will only be able to locate the DLL if is exists in the same directory.
Obviously, this is not an ideal situation. Far from it actually, since you can hardly expect to ship your Delphi 7 executable together with the .NET assembly and tell your customer to leave them in the same directory. This has been solved .NET, and quite well actually, using the Global Assembly Cache (GAC). The main purpose of the Global Assembly Cache is to store (potential different versions of) assemblies, so they can be used and shared by client applications on your .NET machine. So to avoid the dependency problem (of having to put Euro42.dll in the same directory as the Delphi 7 executable), we have to deploy the Euro42.dll assembly in the GAC.
Unfortunately, this means some changes in the C# source code, since we cannot just deploy anything in the GAC. We have to mark the assembly with the AssemblyKeyFile attribute, pointing to the file eBob42.snk that holds the strong key. The latter can be generated with the Strong Name utility sn as follows:

  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
The file eBob42.snk must remain in the same directory where the source file Euro42.cs is located, and we have to refer to the eBob42.snk file in the AssemblyKeyFile attribute of Euro42.cs. The first few lines of this C# source file will be changed as follows (two lines with the // GAC comment were added):
  using System;
  using System.Runtime.InteropServices;
  using System.Reflection; // GAC

  [assembly:AssemblyKeyFile("eBob42.snk")] // GAC
  namespace eBob42 {
  ... // see first listing
  }
We have to recompile the Euro42.cs source again. This results in a Euro42.dll that cannot be used anymore with the Delphi 7 project (I reckon that the signature of the assembly has changed with the strong key). As a consequence, we have to regenerate the type library by calling regasm again (just like we did before). And that will change all GUIDs in the type library (compared to the first one), so we also have to re-import the Euro42.tlb file inside Delphi 7 again, producing a new Euro42_TLB.pas (if you compare this new version to the first version, you'll see that the only things that have changed are the GUIDs).
After recompiling the Delphi 7 application with the new Euro42_TLB.pas file, everything works fine again - as long as the Euro42.dll is in the same directory as the Delphi 7 executable. In order to make sure we can place the Delphi 7 executable anywhere we want (without the need for the presence of the Euro42.dll), we still have to actually deploy the Euro42.dll in the Global Assembly Cache (we did the preparation, but haven't actually deployed it, yet). This can be done with the gacutil command-line tool, as follows:
  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
After calling gacutil, the directory C:\WinNT\assembly\GAC contains a new subdirectory called Euro42, with another subdirectory called 0.0.0.0__0ad170caf360281a (this is my public key token). The subdirectory contains both the Euro42.dll and an __AssemblyInfo__.ini file with the following contents:
  [AssemblyInfo]
  MVID=95374403e3e08d498a21e8ec69b1d1e8
  URL=file:///D:/src/Euro42.dll
  DisplayName=Euro42, Version=0.0.0.0, Culture=neutral,
    PublicKeyToken=0ad170caf360281a
If we change the implementation (but not the signature) of the ToEuro and FromEuro method, we have to recompile the Euro42.cs source code, and call gacutil again to put the new version of the Euro42.dll in the Global Assembly Cache. Note that you don't have to call the regasm (to generate and register a new type library) unless you've changed the signature of the Euro42 assembly.
In order to remove the Euro42.dll assembly from the Global Assembly Cache we have to call gacutil with the -u flag and the name Euro42 as arguments (note that we should not include the .dll extension at that time - been there, done that, got the error message).
  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
But before removing the Euro42.dll test assembly, we should first test the Delphi 7 project in an otherwise empty directory to illustrate the fact that we now no longer require Euro42.dll to be present in the same directory as the Delphi 7 executable. In fact, you can delete the Euro42.dll (as long as it's still in the Global Assembly Cache, of course).

C# Exceptions
Time to try to select the "Error" currency and see how errors are detected (at the managed .NET side) and handled (at the unmanaged Delphi side). Select the Error currency in the RadioGroup, and click on one of the Euro conversion buttons. This will result in a call to FromEuro or ToEuro with a value of 13 as first argument. Since the C# side uses an array that runs from index 0 to 12, the 13th index will result in an error. Or rather in an exception, thrown by the .NET environment, and nicely received by Delphi, telling us that the supplied index was outside the bounds of the array.

C# out-of-bounds Exception

Is there any way we can influence this exception, and perhaps throw an exception with our own custom error message? In order to find the answer, I changed the implementation of the FromEuro and ToEuro methods in the C# class, as follows:

  // Implement MyInterface methods
  public float FromEuro(int Currency, float Amount) {
    if ((Currency < 0) || (Currency > 12))
      throw new ApplicationException("Invalid Currency"); // custom message
    return Amount * EuroConversionRate[Currency];
  }
  public float ToEuro(int Currency, float Amount) {
    if ((Currency < 0) || (Currency > 12))
      throw new ApplicationException("Invalid Currency"); // custom message
    return Amount / EuroConversionRate[Currency];
  }
True C# fans probably want to derive their own exception type from ApplicationException and use that one instead, but for this little demo the use of ApplicationException should be clear enough. We need to recompile the Euro42.cs source file and re-deploy the Euro42.dll in the Global Assembly Cache (otherwise we'll still use the old version from the GAC).
And if we select the "Error" currency now and click on the FromEuro or ToEuro button, we get a new dialog with the custom exception message that we defined at the C# side. Note that all C# exceptions will be turned into EOleExceptions at the Delphi side, with a copy of the message that was passed at the C# side (in other words: there's no way we could even distinguish between an ApplicationException or another exception at the Delphi side)..

Custom C# Exceptions

Obviously, we can use a try-except block to catch the EOleException at the Delphi side and show an even more detailed (or nicer) error message. That last change can be implemented as follows:

  procedure TForm1.btnFromEuroClick(Sender: TObject);
  begin
    try
      edtValuta.Text := FloatToStr(
        Euro42.FromEuro(Succ(Currency.ItemIndex),StrToFloatDef(edtEuro.Text,0)))
    except
      on E: EOleException do
        ShowMessage(E.ClassName + ' Error: ' + E.Message)
    end
  end;

Delphi 7 and .NET
This ends my first coverage of Delphi 7 and .NET, showing how we can compile, register, import and use .NET assemblies (written in C#). However, Delphi 7 contains more .NET support: it ships with the Delphi for .NET preview command-line compiler, which will be the topic of the following month(s). So stay tuned, and don't hesitate to send me feedback or comments (or suggestions for topics to cover using Delphi for .NET).


This webpage © 2002-2010 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.