Introduction
Recently, my co-author had a requirement where he needed to convert a C# class library into a COM compatible component which could be called from either a C++/C#/Perl/Scripting environment. There was already a lot of information on how to implement this, but he had a somewhat more involved requirement. The requirement was that his Perl script needed to call a C# function (say Analyze()
). The Analyze()
function would typically take a long time to process, and the client needed to be updated regularly with the progress counter. This would be best solved using Callbacks.
However, there was scanty information available in a ready-to-use form, which could explain how to achieve this.
The article below explains how to handle multiple .NET events in a Perl environment.
Background
This article is an attempt to explain the concept of Event Sources and Event Sinks as applicable to a COM client.
The code contains a C# COM Server, and a sample Perl Client which explains in the simplest manner possible, the concept of tapping the power of callback functions.
This article does the above by showing how to use the Win32::OLE
module present in Perl, and using it to trap events generated in the .NET component.
Using the Code
To use the code attached, there are a few steps that need to be performed.
- Unzip the CSharp_and_Scripting_New_src.zip.
- Browse to the CSharp_and_Scripting_New\CallBack Server\CallBack folder.
- Run register.bat
- In case the register.bat script does not work for you, open the project CallBack.sln in Visual Studio 2005. Go to the Tools Menu -> Visual Studio 2005 Command Prompt.
- Change directory in the command window to the CSharp_and_Scripting_New\CallBack Server\CallBack\bin\debug folder.
- Type
regasm CallBack.dll /tlb:Callback.tlb
- type
gacutil -i CallBack.dll
- Now that the .NET C# COM server is registered in your system, you can run the CSharp_and_Scripting_New\CallBack Client\Sample1.pl to view the output as shown below:
Description
COM Server Code
The COM server has been written in C#. C# does not have any direct concept of function pointers. Instead, it defines something called delegate
s which is a safe form of a reference to a function. Now to tap the power of delegate
s, let us define some delegate
s as shown below in our class DotNetEventSender
:
[ComVisible(false)]
public delegate void MyEventTarget(string msg);
[ComVisible(false)]
public delegate void MyEventTarget2(int nTimerCounter);
public event MyEventTarget TheEvent;
public event MyEventTarget2 TheEvent2;
As you can see above, we've declared two delegate
s of different types and associated two events with these delegate
s. Now let us declare an interface which our class will expose:
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface CallBackEventInterface
{
[DispId(1)] void TheEvent(string msg);
[DispId(2)] void TheEvent2(int nTimerCounter);
}
Our class will then expose this interface as shown below:
[ComSourceInterfaces(typeof(CallBackEventInterface))]
[ClassInterface(ClassInterfaceType.None)]
public class DotNetEventSender:IDotNetEventSender
{ ...
Notice the attribute tag:
[ComSourceInterfaces(typeof(CallBackEventInterface))]
This tells the compiler that we are interested in exposing an event source for our class. The event sink will be then implemented in the Perl Client in a package which is expressly consuming (handling) all events generated by our C# server.
Now, it's not much use exposing an Event source, if we don't also implement some server functions that could be called by the Perl client. For example, the Perl client would call the function Run()
which will start the timer in the C# server. This timer would then intermittently fire the event sources, which could then be handled in the Perl client.
So, let's now implement some class functions in the C# server.
private int m_nLoopCounter=0;
public void Run()
{
Console.Out.WriteLine("Inside Run");
System.Timers.Timer progress = new System.Timers.Timer(1000);
progress.Elapsed += new ElapsedEventHandler(TimerFunction);
progress.Start();
}
private void TimerFunction(object source, ElapsedEventArgs e)
{
m_nLoopCounter++;
TheEvent("Hello, World!");
TheEvent2(m_nLoopCounter);
}
Perl Client Code
In the Perl client code, the basic step is to create an object of the C# server as shown below:
my $TM = Win32::OLE->new('CallBack.DotNetEventSender');
Once this is done, we want to inform the C# server that we are interested in receiving notifications when events of interest are generated on the server. We do this by stating:
Win32::OLE->WithEvents($TM, 'MyEvents');
Here, we are informing the C# server that all events should be forwarded to the package MyEvents
. This package will define subroutines with names that are exactly the same as the events in the C# server.
package MyEvents;
#The name of the subroutine should be the exactly the same as the name of the
#event in the C# component.
sub TheEvent
{
my ($obj,$args) = @_;
print "TheEvent() : ".$args."\n";
}
sub TheEvent2
{
my ($obj,$args) = @_;
print "TheEvent2(): Timer Fired Count: ".$args."\n";
}
Now, it's not much use defining all the above code if we don't run a loop, which will keep the Perl client waiting for event messages to arrive. To do this, we add the code to the very end of the main package as shown below:
#keeps the Perl client active, processing the event messages as they
#occur repeatedly.
Win32::OLE->MessageLoop();
That's it, folks! Just run the sample1.pl to see the above logic in action. Happy 'Interop'ing! :-)
History
- 1st February, 2008: Initial post