Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Native (Delphi) callbacks in .NET (C#) COM assembly

0.00/5 (No votes)
26 Apr 2012 1  
How to pass native (Delphi) callback pointer to a .net COM assembly

Introduction

Let's assume that the following are presented:

  • You want to write a .NET method and make it available for native programming languages.
  • This .NET method takes a callback as an argument.
  • You want to call this .NET method from a Delphi application.

Background

First of all, what is a callback? According to Wikipedia, a callback is a reference to a piece of executable code, that is passed as an argument to another code. This allows a lower-level software layer to call a subroutine (or function) defined in a higher-level layer. Dealing with callbacks in the native world is fairly straightforward. Of course in .NET we have delegates which are the closest to this concept, but how do we use/pass callbacks between native and .NET? This is what this article is mainly about. Another issue in the above scenario is making the .NET method visible to the native world. This can be done by making the .NET assembly COM-Visible and here are some important steps when making a .NET COM assembly.

  • Registering the assembly as a COM object.
  • Placing the assembly in the Global Assembly Cache (GAC) if you want to call it from different paths/locations.
  • Giving the assembly a Strong Name if you want to add it to the GAC.

The .NET part

I’ll use Visual Studio 2010 and C# to build this part. In VS2010, add a new class library project and name it CSDemoLibrary. From the project property and in the assembly information dialog, check to make this library COM-Visible.

We need to give this assembly a strong name if we want to add it to the Global Assembly Cash (GAC), to give it this name: Go to the Signing tab and check (Sign The Assembly), then from the drop down list, select <new …> and name the file CSDemoLibrary.

Because we need to pass a pointer to this assembly there is one further step, which is enabling unsafe code in this assembly by checking (Allow Unsafe Code) in the Build tab.

Now we are ready to write the code, delete the default class in the project and add new class, name it MyClass.

For the sake of simplicity, in this demo, we will use a simple callback which takes just two parameters (integer and string). In Delphi this callback function will be declared like this:

procedure callback(intParam: Integer; strParam: pChar);

So, to use the equivalent of this callback in .NET, declare a delegate like this:

public delegate void NativeCallback(Int32 intParam, [MarshalAs(UnmanagedType.LPWStr)] string strParam);

I use the [MarshalAs(UnmanagedType.LPWStr)] attribute here in order for a string to get converted into Unicode/widestring. You can omit this attribute if you want to pass just AnsiString (in that case the strParam in the Delphi method should be pAnsiChar). Now let’s write the .NET method. First we need to make MyClass COM-Visible by using the ComVisible(true) attribute with a unique GUID string and because this .NET method will deal with the native pointer (the callback pointer), we must mark it with the (unsafe) keyword. Here is the MyClass.cs full code:

using System;
using System.Runtime.InteropServices;
using System.Threading;namespace CSDemoLibrary
{
public delegate void NativeCallback(Int32 intParam, 
        [MarshalAs(UnmanagedType.LPWStr)] string strParam);

[ComVisible(true),
GuidAttribute("3A65D04A-3F2F-4CB3-B65A-8D402B8C64CE")]
public class MyClass
{

public unsafe int Process(
int intValue,
Int32 callbackPointer,
int intParam,
string strParam)
{
IntPtr ptr = new IntPtr(callbackPointer);
NativeCallback callbackMethod = 
      (NativeCallback)Marshal.GetDelegateForFunctionPointer(ptr, typeof(NativeCallback));

callbackMethod(intParam, strParam);
Thread.Sleep(1000);

callbackMethod(25, "first step");
Thread.Sleep(1000);
callbackMethod(50, "second step");
Thread.Sleep(1000);
callbackMethod(75, "third step");
Thread.Sleep(1000);
callbackMethod(100, "fourth step");
return intValue * 10;
}
}
}

In the previous code the Process method emulates a long running process (like getting a big chunk of data from the internet) with status notifications using callback from the native host application. In our demo and for the sake of simplicity we use Thread.Sleep to pause the execution for one second each step to emulate a long running process that returns an integer value. In this method callbackPointer is the native callback pointer and (intParam, strParam) are the callback parameters. These two parameters will work as initialization parameters for the callback. In many scenarios there is no need for these initialization parameters but I keep them here to show how we can pass them from a native application to the .NET method. The GetDelegateForFunctionPointer method is the key in this solution; it converts an unmanaged function pointer to a delegate. There is more information about this method here.

Registering the .NET assembly

To avoid paths issue, I’ll use (Visual Studio Command Prompt). There is shortcut to this tool in (Start menu-> All Programs-> Microsoft Visual Studio 2010-> Visual Studio Tools). Using this tool you can register the COM assembly by following these steps:

  • Run Visual Studio Command Prompt as Administrator, then go to the CSDemoLibrary.dll folder using regular DOS commands.
  • If you intend to put the .NET library CSDemoLibrary.dll in the same directory as the native application (.exe), you can ignore the step of adding the assembly to the Global Assembly Cash (GAC), else you should add it to the GAC, and to do so, use gacutil with the parameter –I, like this: gacutil -i CSDemoLibrary.dll.
  • Register the COM assembly using regasm like this: regasm CSDemoLibrary.dll.

Notes:

  • To remove the .NET assembly from GAC: gacutil -u CSDemoLibrary.
  • To unregister the .NET assembly: regasm -u CSDemoLibrary.dll.

The native (Delphi) part

I’ll use Delphi 2010 for this part. In the Delphi 2010 IDE, create a new VCL application. Add (TLabel, TButton, TGauge) to the main form.

In the code behind of the main form add ComObj to the uses section. Make a simple callback procedure to update Gauge1 progress and the lbMessage caption like this:

procedure callback(intParam: Integer; strParam: pChar); stdcall;
Begin
 Form2.Gauge1.Progress := intParam;
 Form2.lbMessage.Caption := strParam;
 Application.ProcessMessages;
End;

I’ll use late binding in this demo, so update the btnProcess click event handler like this:

procedure TForm2.btnProcessClick(Sender: TObject);
var
oleObject: OleVariant;
begin
try
oleObject := CreateOleObject('CSDemoLibrary.MyClass');
ShowMessage('result= ' + IntToStr(oleObject.Process(10, LongInt(@callback), 0, 'initialization')));
except on E: Exception do
ShowMessage('COM Error: ' + #13 + #10 + e.Message);
end;
end;

This application calls the .NET long process which notifies the native application about the process status through the callback procedure and finally it displays the result of the .NET method/process.

References

Points of Interest

I hope this article shows how easy is is to consume a C# COM assembly in Delphi.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here