Friday, 30 November 2012

Accessing .NET code from Delphi using COM Interoperability

Introduction

When .NET Framework has been introduced, it included number of options to interoperate with unmanaged code. From Delphi developer perspective most notable seems to be COM Interoperability. COM Interoperability allows to use COM object within managed code and well as managed code can be exposed as COM and used in COM capable native application. In other words - COM library created with Delphi can used within .Net application as well as .Net code can be accessed as COM within Delphi.

While importing COM libraries created with Delphi is relatively simple, using .NET assemblies in Delphi can be tricky. Firstly, .NET assembly needs to follow certain guidelines, then there's an issue of importing the assembly into a Delphi project and finally careful consideration needs to be put into deployment. Here's a routine which I believe is best in most scenarios. Apart from providing an effective solution, it's intended to reduce unnecessary clutter in purpose to achieve clean and easy to maintain code.

Preparing .NET assembly

Only public classes, interfaces and enumeration can be exposed to be used in COM. What is actually exposed can be controlled by ComVisible attribute, which can be applied to assemblies, interfaces, classes, structures, delegates, enumerations, fields, methods and properties. I believe it's better to disable COM visibility at the assembly level, so nothing is exported, unless explicitly specified. This arrangement allows for fine tuning resulting COM library according to the principle of minimal clutter.

To disable COM visibility for an assembly make sure the following line exists in AssemblyInfo.cs
[assembly: ComVisible(false)].
Note that ticking off "Make assembly COM-Visible" checkbox in the Project Properties modifies AssemblyInfo.cs accordingly.

All parameters and return values must be either a simple type or a type with its ComVisible attribute set to true. Interoperable classes should have a public parameterless constructor.

GuidAttribute can be applied to assemblies, interfaces, classes, enumerations, structures, or delegates and it's used to assign Guid to an exported COM entity. Although not strictly required, it's needed to access interfaces and classes by Guid. If it's not present, then the Guid is assigned at the registration time, so it's different every time registration takes place.

Important feature of COM Interoperability are class interfaces.  When a class is "ComVisible", its class interface exposes all public methods, properties and events of the class. How class interface is exposed is controlled by ClassInterface attribute. It's better to define an explicit interface rather than use the class interface. Firstly, if you make changes to the class, there won't be necessary to re-import. Also, while using explicit interfaces, only methods that are needed are exposed. If class interfaces are not used, they should be excluded from the type library by applying the ClassInterface attribute with ClassInterfaceType.None parameter.

As an example, let's try to access .Net Tracing using Delphi. ComVisible, Guid and ClassInterface attributes are used according to the guidelines above.

namespace Altaira.Utils
{
    [ComVisible(true)]
    [Guid("4A537FBB-8123-48A0-8E37-7D0560198927")]
    public interface IDTrace
    {
        void WriteLine(string message);
        void TraceError(string message);
        void TraceWarning(string message);
    }

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("9A263B40-93FC-4AA3-876A-B8BFDBA2C55D")]
    public class DTrace : IDTrace
    {
        public void WriteLine(String message)
        {
            Trace.WriteLine(message);
        }

        public void TraceError(string message)
        {
            Trace.TraceError(message);
        }

        public void TraceWarning(string message)
        {
            Trace.TraceWarning(message);
        }
    }
}

Registering and importing the assembly

There's number of ways to register an assembly as COM library. The simplest is to tick the Register for COM Interop checkbox in the Project Properties - the assembly will then be registered as part of the build process.
To be able to manage registration in various scenarios, it's best to utilise the command line utility called Regasm. It's part of the .NET framework and can be found at c:\windows\microsoft.net\framework\(version). It doesn't matter which version of the .NET Framework do you use, as long as Regasm version is not lesser than CLR version. For example, it's not possibly to register assembly build with .NET Framework 4.0 using Regasm from version 2.0.

For example, the following command registers DTraceUtils.dll assembly as COM

<Path>\Regasm DTraceUtils.dll

Regasm can also create a type library by using the /tlb option. The following command creates and registers type library called DTraceUtils.tlb (in addition to registering DTraceUtils.dll assembly):

<Path>\Regasm /tlb DTraceUtils.dll

Note that the same could be achieved using tlibexp utility.

Once a Type Library is created and registered, it's all the same as regular COM. From the Component menu select Import Component, then Import Type Library. Note the Palette Page dropdown on Import Component dialog. It determines the page on the Component Palette, where VCL component wrapper will be installed; if left blank, the component wrapper will not be generated. Leaving it blank is the preferred option, as except for certain specific scenarios, there's no benefit in creating a component wrapper.

Following the above guidelines results in neat and easy to manage code.

  IDTrace = interface(IDispatch)
    ['{4A537FBB-8123-48A0-8E37-7D0560198927}']
    procedure WriteLine(const message: WideString); safecall;
    procedure TraceError(const message: WideString); safecall;
    procedure TraceWarning(const message: WideString); safecall;
  end;
  CoDTrace = class
    class function Create: IDTrace;
    class function CreateRemote(const MachineName: string): IDTrace;
  end;
It's important to remember to recreate Type Library every time every interface signature changes.

Delphi code

Assuming IDTrace is a reference to explicit interface as per following declaration
var
  Trace: IDTrace;

DTrace can be instanciated using the following code.
  Trace := CoDTrace.Create;

Note the simplicity of instancing the interface using CoClass. If VCL component wrapper was used instead, you would have much less control over creation and lifetime of COM objects.
The interface reference can be used to access the Trace functionality
  Trace.WriteLine('Using System.Diagnostics from Delphi code');

This is actually quite useful example, assuming more methods are added to DTrace class.
To view logged messages you should create MyApp.exe.config (where myApp.exe is the Delphi app) which looks similar to following:
<?xml version="1.0" encoding="utf-8">
<configuration>
  <system.diagnostics>
    <trace autoflush="true">
      <listeners>
        <add name="text"
                    type="System.Diagnostics.TextWriterTraceListener"
             initializeData="c:\projects\data\TextOutput.log"/>
       </listeners>
    </trace>
  </system.diagnostics>
</configuration>

Deployment

Major concern while deploying an interoperable assembly is registering it with COM. The easy option is to make use of a setup project. After the assembly is added to the project, set the Register property determines COM registration options and installation process takes care of it.

If you need flexibility and control, it's better to use Regasm utility.

It's very important to realise that registration by default doesn't include the full path to the assembly executable. When COM is accessed, the system is looking for the assembly in the current directory then in the Global Assembly Cache. Sometimes it might be better to put the assembly in the GAC, but if assembly is not in GAC it needs to be in the current directory. Some might consider it a limitation, but I personally don't think it's the case.


Wednesday, 24 October 2012

Welcome

Hi All,

As a Delphi supporter for many years, I'm happy to notice, that for the first time in many years, new developments in Delphi world looks actually quite promising. It's has always been the best tool for desktop development, but one wouldn't necessarily choose it to develop a web service. Delphi can find its niche, but should able to fit into heterogeneous environment and interoperate smoothly with other software development frameworks, like .Net or Java.

I'd like to share ideas on COM Interop, WCF in Delphi, WinForms and other technologies related to both Delphi and .Net.

To be continued.