summaryrefslogtreecommitdiff
path: root/src/mscorlib/src/System/Runtime/InteropServices/ComEventsHelper.cs
blob: 0bf616d94ce8419fe07a1c6bb4185ba9a3a41212 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.


/*============================================================
**
**
** Purpose: ComEventHelpers APIs allow binding 
** managed delegates to COM's connection point based events.
**
**/
namespace System.Runtime.InteropServices {
    //
    // #ComEventsFeature
    // 
    // code:#ComEventsFeature defines two public methods allowing to add/remove .NET delegates handling
    // events from COM objects. Those methods are defined as part of code:ComEventsHelper static class
    // * code:ComEventsHelper.Combine - will create/reuse-an-existing COM event sink and register the
    //     specified delegate to be raised when corresponding COM event is raised
    // * code:ComEventsHelper.Remove
    // 
    // 
    // To bind an event handler to the COM object you need to provide the following data:
    //  * rcw - the instance of the COM object you want to bind to
    //  * iid - Guid of the source interface you want the sink to implement
    //  * dispid - dispatch identifier of the event on the source interface you are interested in
    //  * d - delegate to invoked when corresponding COM event is raised.
    // 
    // #ComEventsArchitecture:
    // In COM world, events are handled by so-called event sinks. What these are? COM-based Object Models
    // (OMs) define "source" interfaces that need to be implemented by the COM clients to receive events. So,
    // event sinks are COM objects implementing a source interfaces. Once an event sink is passed to the COM
    // server (through a mechanism known as 'binding/advising to connection point'), COM server will be
    // calling source interface methods to "fire events" (advising, connection points, firing events etc. -
    // is all COM jargon).
    // 
    // There are few interesting obervations about source interfaces. Usually source interfaces are defined
    // as 'dispinterface' - meaning that only late-bound invocations on this interface are allowed. Even
    // though it is not illegal to use early bound invocations on source interfaces - the practice is
    // discouraged because of versioning concerns.
    // 
    // Notice also that each COM server object might define multiple source interfaces and hence have
    // multiple connection points (each CP handles exactly one source interface). COM objects that want to
    // fire events are required to implement IConnectionPointContainer interface which is used by the COM
    // clients to discovery connection poitns - objects implementing IConnectionPoint interface. Once
    // connection point is found - clients can bind to it using IConnectionPoint::Advise (see
    // code:ComEventsSink.Advise).
    // 
    // The idea behind code:#ComEventsFeature is to write a "universal event sink" COM component that is
    // generic enough to handle all late-bound event firings and invoke corresponding COM delegates (through
    // reflection).
    // 
    // When delegate is registered (using code:ComEventsHelper.Combine) we will verify we have corresponding
    // event sink created and bound.
    // 
    // But what happens when COM events are fired? code:ComEventsSink.Invoke implements IDispatch::Invoke method
    // and this is the entry point that is called. Once our event sink is invoked, we need to find the
    // corresponding delegate to invoke . We need to match the dispid of the call that is coming in to a
    // dispid of .NET delegate that has been registered for this object. Once this is found we do call the
    // delegates using reflection (code:ComEventsMethod.Invoke).
    // 
    // #ComEventsArgsMarshalling
    // Notice, that we may not have a delegate registered against every method on the source interface. If we
    // were to marshal all the input parameters for methods that do not reach user code - we would end up
    // generatic RCWs that are not reachable for user code (the inconvenience it might create is there will
    // be RCWs that users can not call Marshal.ReleaseComObject on to explicitly manage the lifetime of these
    // COM objects). The above behavior was one of the shortcoimings of legacy TLBIMP's implementation of COM
    // event sinking. In our code we will not marshal any data if there is no delegate registered to handle
    // the event. (code:ComEventsMethod.Invoke)
    // 
    // #ComEventsFinalization:
    // Additional area of interest is when COM sink should be unadvised from the connection point. Legacy
    // TLBIMP's implementation of COM event sinks will unadvises the sink when corresponding RCW is GCed.
    // This is achieved by rooting the event sinks in a finalizable object stored in RCW's property bag
    // (using Marshal.SetComObjectData). Hence, once RCW is no longer reachable - the finalizer is called and
    // it would unadvise all the event sinks. We are employing the same strategy here. See storing an
    // instance in the RCW at code:ComEventsInfo.FromObject and undadvsing the sinks at
    // code:ComEventsInfo.~ComEventsInfo
    // 
    // Classes of interest:
    // * code:ComEventsHelpers - defines public methods but there are also a number of internal classes that
    //     implement the actual COM event sink:
    // * code:ComEventsInfo - represents a finalizable container for all event sinks for a particular RCW.
    //     Lifetime of this instance corresponds to the lifetime of the RCW object
    // * code:ComEventsSink - represents a single event sink. Maintains an internal pointer to the next
    //     instance (in a singly linked list). A collection of code:ComEventsSink is stored at
    //     code:ComEventsInfo._sinks
    // * code:ComEventsMethod - represents a single method from the source interface which has .NET delegates
    //     attached to it. Maintains an internal pointer to the next instance (in a singly linked list). A
    //     collection of code:ComEventMethod is stored at code:ComEventsSink._methods
    //     
    // #ComEventsRetValIssue:
    // Issue: normally, COM events would not return any value. However, it may happen as described in
    // http://support.microsoft.com/kb/810228. Such design might represent a problem for us - e.g. what is
    // the return value of a chain of delegates - is it the value of the last call in the chain or the the
    // first one? As the above KB article indicates, in cases where OM has events returning values, it is
    // suggested that people implement their event sink by explicitly implementing the source interface. This
    // means that the problem is already quite complex and we should not be dealing with it - see
    // code:ComEventsMethod.Invoke

    using System;
    using System.Runtime.Remoting;

    /// <summary>
    /// The static methods provided in ComEventsHelper allow using .NET delegates to subscribe to events
    /// raised COM objects.
    /// </summary>
    public static class ComEventsHelper {

        /// <summary>
        /// Adds a delegate to the invocation list of events originating from the COM object.
        /// </summary>
        /// <param name="rcw">COM object firing the events the caller would like to respond to</param>
        /// <param name="iid">identifier of the source interface used by COM object to fire events</param>
        /// <param name="dispid">dispatch identifier of the method on the source interface</param>
        /// <param name="d">delegate to invoke when specifed COM event is fired</param>
        [System.Security.SecurityCritical]
        public static void Combine(object rcw, Guid iid, int dispid, System.Delegate d) {

            rcw = UnwrapIfTransparentProxy(rcw);

            lock (rcw) {
                ComEventsInfo eventsInfo = ComEventsInfo.FromObject(rcw);

                ComEventsSink sink = eventsInfo.FindSink(ref iid);
                if (sink == null) {
                    sink = eventsInfo.AddSink(ref iid);
                }


                ComEventsMethod method = sink.FindMethod(dispid);
                if (method == null) {
                    method = sink.AddMethod(dispid);
                }

                method.AddDelegate(d);
            }
        }

        /// <summary>
        /// Removes a delegate from the invocation list of events originating from the COM object.
        /// </summary>
        /// <param name="rcw">COM object the delegate is attached to</param>
        /// <param name="iid">identifier of the source interface used by COM object to fire events</param>
        /// <param name="dispid">dispatch identifier of the method on the source interface</param>
        /// <param name="d">delegate to remove from the invocation list</param>
        /// <returns></returns>
        [System.Security.SecurityCritical]
        public static Delegate Remove(object rcw, Guid iid, int dispid, System.Delegate d) {

            rcw = UnwrapIfTransparentProxy(rcw);

            lock (rcw) {

                ComEventsInfo eventsInfo = ComEventsInfo.Find(rcw);
                if (eventsInfo == null)
                    return null;
                ComEventsSink sink = eventsInfo.FindSink(ref iid);
                if (sink == null)
                    return null;
                ComEventsMethod method = sink.FindMethod(dispid);
                if (method == null)
                    return null;

                method.RemoveDelegate(d);

                if (method.Empty) {
                    // removed the last event handler for this dispid - need to remove dispid handler
                    method = sink.RemoveMethod(method);
                }
                if (method == null) {
                    // removed last dispid handler for this sink - need to remove the sink
                    sink = eventsInfo.RemoveSink(sink);
                }
                if (sink == null) {
                    // removed last sink for this rcw - need to remove all traces of event info
                    Marshal.SetComObjectData(rcw, typeof(ComEventsInfo), null);
                    GC.SuppressFinalize(eventsInfo);
                }

                return d;
            }
        }

        [System.Security.SecurityCritical]
        internal static object UnwrapIfTransparentProxy(object rcw) {
#if FEATURE_REMOTING
            if (RemotingServices.IsTransparentProxy(rcw)) {
                IntPtr punk = Marshal.GetIUnknownForObject(rcw);
                try {
                    rcw = Marshal.GetObjectForIUnknown(punk);
                } finally {
                    Marshal.Release(punk);
                }
            }
#endif
            return rcw;
        }
    }

}