Event Handling in IPRO
IPRO is an in-proc and an out-of-proc COM IPROPLUS server. IPRO applications are basically the clients of any of these servers.
It is generally true that applications using COM resources are more stable and robust the less they communicate with COM objects. Usually some instruction is issued to the COM object and during the execution of this instruction such events might be generated about which the COM object wants to inform its client. If the client pays attention to these events, the communication immediately becomes two-way which might generate several performance and stability related problems.
When COM server objects declare their sending of events to the client, then virtually they declare an interface along with all its functions and they assume responsibility for the object calling the relevant function of the interface when an event happens, insofar as the programmer implements such an interface and hands over a copy of it to the COM object. The interface is called a source interface in accordance with the COM naming standard. An object can declare more than one source interface out of which one will be marked as default.
From the object's point of view each source interface is equal, but certain development environments handle the default source interface with special attention, they usually provide built-in help to the implementation of these.
Traditionally, the source interfaces of the COM (OLE era, ActiveX) are dispinterfaces (dispatch or data-driven interfaces) that basically inherit their structures from the IDispatch interface (and hence correspond to them as well). They have seven function entry points defined by the IDispatch interface that can be called. Further declared functions (that correspond to the individual events) do not have entry points, and they cannot be called directly, but only through the IDispatch::Invoke function. The function to call must be defined for the Invoke function and the parameter list of the required function must be handed over (data driven operation).
During its running, the Invoke function must check whether the function exists or not, whether the given parameter list corresponds to the syntax of the called function in terms of type and number and finally it has to call the given function.
This run-time evaluation is called late-binding. If a COM object broadcasts a given event very often, the fact of late-binding might considerably deteriorate the speed of the application. Consequently, though dispinterfaces are unbelievably flexible, they do cause significant performance loss.
Due to the performance problems IPRO objects regularly declare dual source interfaces, one being a dispinterface marked as default because of traditional reasons, the other being an interface originating from the IUnknown interface. The signature of the functions (events) is identical on both source interfaces. The unknown-based source interface has the advantage of each function possessing its own entry point. The code required for calling them is generated at program building hence there is no run-time evaluation; this is called early-binding.
However, their implementation differs from the implementation of the dispinterfaces, in C++ an object needs to be implemented that possesses the given source interface, while in .NET the non-default source interface must be used. This makes the implementation of individual event handlers a little more circumvented.
The threading model causes further problems. IPRO is basically free-threaded, each interface it issues can be used on each thread without limitations. However, IPRO itself is a multi-threaded application, often it runs hundreds of threads simultaneously. Event to be broadcasted can be created on any of the threads. IPRO tries to call the relevant functions of the source interfaces handed over at the time of event creation, but it might encounter obstacles due to the restrictions of the COM. The source interfaces issued by the clients are usually single or apartment threads and hence their functions can only be called from foreign (created by IPRO) threads with certain restrictions. The source interface that was handed over must support the Imarshal interface, or the caller (in this case IPRO) has to marshal the source interface with the appropriate functions of the COM to the thread where it wants to use it. IPRO automatically performs this marshalling in the case of the dispinterfaces (with this causing further performance deterioration) but not in the case of IUnknown based interfaces. The IUnknown based source interfaces must support the IMarshall interface. This is not a problem in .NET, since the .NET interfaces automatically support IMarshall due to the language support feature, but for C++ programmers this assigns further tasks. They have to implement the IMarshall interface (not a trivial venture) or alternatively they can consider the usage of the CoCreateFreeThreadedMarshaler Windows API call. In this latter case however, the application itself must also be thread-safe.
It is worth considering that the more events are handled by the client, the greater the chance of deadlock becomes especially if the source interfaces are also free-threaded. If the application has a UI, this gets even more difficult and the programmer updates the UI under the control of events which ususally means a thread switch to the application's main thread. The synchronous handling of events can easily cause immediate deadlock (which can be well reproduced and amended as a bug) but can also take the application towards such a deadlock situation when the end-user's innocent mouse-click triggers the deadlock.
Usually it seems a great approach for the programmer to handle IPRO events - always being synchronous calls - asynchronously, i.e. he/she lets the function return immediately to the caller and performs most of the work (e.g.: updating the UI) at a later time or on a different thread. It must also be noted that it is not forbidden to call back any function of any IPRO objects during event handling, but this attempt can be dangerous.
The following performance consideration is only relevant for .NET programmers. A .NET programmer does not implement source interfaces, but event handlers. However, in the background the .NET implements the whole interface in a way that it only connects one entry point with the event handler and returns to the caller immediately with the rest of the entry points. This raises a problem if the source interface declares more functions (events). Then handling every single event means handing over another source interface to the COM object and there is only one useful function implementation on each interface. But the COM object is not aware of this and supposing that a source interface declares N number of events and the .NET client wants to handle all of them, one single event is handled the following way: the COM object calls the given function on N-1 interface absolutely uselessly which though return immediately, marshalling and the possible late-binding already took the time and exactly one of its calls does in fact arrive to the .NET event handler. This is very inefficient. For this reason, .NET programmers should consider whether to implement the whole source interface instead of the event handlers.
.NET-specific issues
The options summarized:
-
For C++ programmers:
Implementing the dispinterface: Relatively trivial (straightforward) with extremely bad performance.
Implementing the IUnknown based interface: Implementation of the interface is relatively trivial. IMarshal interface must also be implemented. In case of using the CoCreateFreeThreadedMarshaler the thread-safe state of the application must be ensured and the risk of deadlock also rises. Performance is optimal.
-
For .NET programmers:
Defining the event handler with the usage of the dispinterface: Trivial. Performance is very bad, but it can be a good option if the event is rare and the dispinterface declares few events.
Defining the event handler with the usage of the IUnknown interface: Not that trivial but performance is better.
Implementing the whole IUnknown based source interface: Not trivial at all (requires sophisticated job). Performance is optimal.
-
Examples for the .NET approach:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using IproPlus; using System.Runtime.InteropServices.ComTypes; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Pages pages; //get a Pages object
-
Solution 1
pages.PageInserted += new DPagesEvents_PageInsertedEventHandler(pages_PageInserted);
-
Solution 2
((IPagesEvents_Event)pages).PageInserted += new IPagesEvents_PageInsertedEventHandler(Program_PageInserted);
-
Solution 3
IConnectionPoint cp; int cookie = 0; IConnectionPointContainer cpc = (IConnectionPointContainer)pages; cpc.FindConnectionPoint(typeof(IPagesEvents).GUID, out cp); cp.Advise(new MyPagesEvents(), out cookie); //Do sth, eg. Load a page
-
Solution 1 (cleanup)
pages.PageInserted -= new DPagesEvents_PageInsertedEventHandler(pages_PageInserted);
-
Solution 2 (cleanup)
((IPagesEvents_Event)pages).PageInserted -= new IPagesEvents_PageInsertedEventHandler(Program_PageInserted);
-
Solution 3 (cleanup)
cp.Unadvise(cookie); } static void Program_PageInserted(Pages Pages, int PageUniqueID, int PageIndex) { } static void pages_PageInserted(Pages Pages, int PageUniqueID, int PageIndex) { } } public class MyPagesEvents : IPagesEvents { public void ActivePageChanged(Pages Pages, int OldPageUniqueID, int NewPageUniqueID, int OldPageIndex, int NewPageIndex { } public void ImageChanged(Pages Pages, int PageUniqueID, int PageIndex) { } public void PageInserted(Pages Pages, int PageUniqueID, int PageIndex) { } public void PageMoved(Pages Pages, int PageUniqueID, int OldPageIndex, int NewPageIndex) { } public void PageRemoved(Pages Pages, int PageUniqueID, int PageIndex) { } public void PageStateChanged(Pages Pages, int PageUniqueID, int PageIndex, PAGESTATE OldPageState, PAGESTATE NewPageState) { } public void PagesMoved(Pages Pages, Array PageUniqueIDs, Array OldPageIndices, Array NewPageIndices) { } public void PagesRemoved(Pages Pages, Array PageUniqueIDs, Array PageIndices) { } public void PagesReordered(Pages Pages) { } public void StatisticsChanged(Pages Pages, int PageUniqueID, int PageIndex) { } } }