Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
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 successfullyAt 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.
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.
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.
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!
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.
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.snkThe 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).
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 cacheAfter 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=0ad170caf360281aIf 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.
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 = 0But 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.
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).
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).