.Net interop - using C# assembly from Delphi Win32

.Net assembly

Let's start explore .Net interop by using sample assembly written in C#. Source code (password "interop") for this example is located in directory ".Net interop - C#".

At first, compile .Net assembly. You may correct build.bat to specify correct directory to C# compiler (csc.exe) that is include in .Net framework. You should get CSharpAssembly.dll. Copy this file into Test directory - this is needed for last example.

Now start InteropUtils.exe from Tools directory. Browse to CSharpAssembly.dll and press Export button - this will import .Net assembly into COM type library and import this type library into Delphi Win32 unit (Delphi 7, 6 or 5 should be installed). You get also a number of Delphi units for all assemblies that are referenced from .Net assembly - delete them except mscorlib_tlb - this one is used. Remove also all unneeded units from uses in CSharpAssembly_tlb and leave only Windows, ActiveX, mscorlib_tlb, Variants (if you use Delphi 6 or higher).

Let's explore what we have:

mscorlib_tlb.pas - imported Delphi Win32 unit for main .Net assembly is correct but needs some changes - for example, it defines types Pointer and Exception and will produce some problems for use of the same Delphi types. You may change this file manually or download and install Managed extensions for VCL - units for main .Net assemblies are included even in trial version.

CSharpAssembly_tlb.pas - this unit contains definition for all public and COM visible classes and interfaces in sample assembly.

In C# code all classes and interfaces are declared with Guid attribute - this ensures that we will have the same Guid when we recompile .Net assembly. Some classes are declared with ClassInterface attribute - this specifies will class be exported as dispinterface, as dual interface or with one of its interfaces. C# Classes that are exposed to use from unmanaged code are also inherit from ManagedByRefObject for better interop management.

Interfaces that are imported from .Net assembly inherit from IUnknown or IDispatch depending on value of InterfaceType attribute, IDispatch by default and do not contain methods of ancestor (in .Net) interfaces. An example are ITestInterface and ITestInterface2 in CSharpAssembly. To use methods of ancestor interfaces in managed code you should query those interfaces.

Classes exposed to COM must have public constructor without parameters. If there is no such constructor, you could initiate such object from helper .Net class or use Managed extensions for VCL that could initiate .Net objects using parametrized contructor and do much more.

Now let's look how different data types are exported into type library. Strings are exported as WideStrings, integers, doubles as their corresponding types, array as PSafeArray. Objects are exported as interface pointers except one case - "object" type itself is exported as OleVariant because it can hold not only objects but also strings, numbers and dates (in .Net these types are objects).

In "InteropUtils.exe" select "Set codebase" checkbox and press "Register for COM" button. This will register .Net assembly as COM object in Windows registry. This operation is analogic to executing "regasm /codebase CSharpAssembly.dll" from command line. "Set codebase" checkbox (/codebase parameter) is used to access .Net assembly from its current location, otherwise assembly should be placed in Global Assembly Cache (GAC).

Test Delphi Win32 application

Now copy CSharpAssembly_tlb.pas (and mscorlib_tlb.pas if you have no Managed extensions for VCL) into Test directory where test Delphi Win32 project is located. Compile and start this project.

In Delphi Win32 .Net objects could be used with interfaces their expose (button "Test interface"), with auto-generated dual interface (button "Test object") or using dispinterface (button "Test variant"). First method is the best because it ensures procedures order in interface and will be the same when you recompile .Net assembly. Last method is worsth and slowest because it calls functions and procedures usign IDispatch and function names.

Procedure in  handler for "Test array" button shows the use of arrays for .Net interop. Arrays are exported from .Net as PSafeArray that is compatible with OleVariant arrays created with VarArrayCreate() function. .Net framework is very sensitive to data type of elements in this array. For strings, it should be varOleStr, for numbers - corresponding data type (be sure that you pass correct type for integers - Integer, SmallInt or ShortInt). Elements in array of objects are of type varUnknown except array of elements with exact type "object" (object[]) - it is represented as varArray or VarVariant because object in .Net could be string, number or any other type.

Procedure in handler for "Test exception" button shows that .Net exceptions could be catched in unmanaged code (and vice-versa). You could distinguish exceptions by their ErrorCode (HRESULT).

Procedure in handler for "Test form" button shows solution for problem that may occur when creating .Net form or control. When such form is created an exception with message "Arithmetic error" could be raised. Exception is raised because Borland and Microsoft compilers have different FPU settings.  To prevent this exception, FPU should be set to not raise floating-point exceptions. The same code is used in CreateWindow() function in Windows unit. Setting FPU to MCW_EM is recommended (and somtimes required) for DirectX and OpenGL.

Initiate .Net object without COM

It is possible to create instance of .Net class without registering it for COM. In this case .Net assembly should be located in GAC or in project directory. T o do this us e ClrCreateManagedInstance() function exported from mscoree.dll - main .Net file (like ole32.dll for COM) . This function works like CoCreateInstance() for COM objects.

Example in handler for "Create .Net object without COM" button uses external keyword but it is possible to load mscoree.dll with LoadLibrary() function and get address of ClrCreateManagedInstance() with GetProcAddress().

When calling ClrCreateManagedInstance(), supply full class name (including namespace) and assembly name, such as: "Interop.Test.InterfacedClass, CSharpAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b4eba4c5f06ce523" and interface you want to get - IUnknown, IDispatch or any interface that is implemented in .Net class and is COM visible. .Net object needs to have public contructor without parameters.

Managed extensions for VCL provide much more powerfull way to instantiate .Net objects including calling parametrized contructors and loading .Net assemblies located in any file and even from streams.

Conclusion

If you have any questions or comments, you are welcome in our forums.