summaryrefslogtreecommitdiff
path: root/src/mscorlib/src/System/Runtime/InteropServices/WindowsRuntime/EventRegistrationTokenTable.cs
blob: 03b17d9261b5166ca90c500b8dfaaa6b36eefec2 (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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// 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.

//

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Threading;

namespace System.Runtime.InteropServices.WindowsRuntime
{
    // An event registration token table stores mappings from delegates to event tokens, in order to support
    // sourcing WinRT style events from managed code.
    public sealed class EventRegistrationTokenTable<T> where T : class
    {
        // Note this dictionary is also used as the synchronization object for this table
        private Dictionary<EventRegistrationToken, T> m_tokens = new Dictionary<EventRegistrationToken, T>();

        // Cached multicast delegate which will invoke all of the currently registered delegates.  This
        // will be accessed frequently in common coding paterns, so we don't want to calculate it repeatedly.
        private volatile T m_invokeList;

        public EventRegistrationTokenTable()
        {
            // T must be a delegate type, but we cannot constrain on being a delegate.  Therefore, we'll do a
            // static check at construction time
            if (!typeof(Delegate).IsAssignableFrom(typeof(T)))
            {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EventTokenTableRequiresDelegate", typeof(T)));
            }
        }

        // The InvocationList property provides access to a delegate which will invoke every registered event handler
        // in this table.  If the property is set, the new value will replace any existing token registrations.
        public T InvocationList
        {
            get
            {
                return m_invokeList;
            }

            set
            {
                lock (m_tokens)
                {
                    // The value being set replaces any of the existing values
                    m_tokens.Clear();
                    m_invokeList = null;

                    if (value != null)
                    {
                        AddEventHandlerNoLock(value);
                    }
                }
            }
        }

        public EventRegistrationToken AddEventHandler(T handler)
        {
            // Windows Runtime allows null handlers.  Assign those a token value of 0 for easy identity
            if (handler == null)
            {
                return new EventRegistrationToken(0);
            }

            lock (m_tokens)
            {
                return AddEventHandlerNoLock(handler);
            }
        }

        private EventRegistrationToken AddEventHandlerNoLock(T handler)
        {
            Contract.Requires(handler != null);

            // Get a registration token, making sure that we haven't already used the value.  This should be quite
            // rare, but in the case it does happen, just keep trying until we find one that's unused.
            EventRegistrationToken token = GetPreferredToken(handler);
            while (m_tokens.ContainsKey(token))
            {
                token = new EventRegistrationToken(token.Value + 1);
            }
            m_tokens[token] = handler;

            // Update the current invocation list to include the newly added delegate
            Delegate invokeList = (Delegate)(object)m_invokeList;
            invokeList = MulticastDelegate.Combine(invokeList, (Delegate)(object)handler);
            m_invokeList = (T)(object)invokeList;

            return token;
        }

        // Get the delegate associated with an event registration token if it exists.  Additionally,
        // remove the registration from the table at the same time.  If the token is not registered,
        // Extract returns null and does not modify the table.
        [System.Runtime.CompilerServices.FriendAccessAllowed]
        internal T ExtractHandler(EventRegistrationToken token)
        {
            T handler = null;
            lock (m_tokens)
            {
                if (m_tokens.TryGetValue(token, out handler))
                {
                    RemoveEventHandlerNoLock(token);
                }
            }

            return handler;
        }

        // Generate a token that may be used for a particular event handler.  We will frequently be called
        // upon to look up a token value given only a delegate to start from.  Therefore, we want to make
        // an initial token value that is easily determined using only the delegate instance itself.  Although
        // in the common case this token value will be used to uniquely identify the handler, it is not
        // the only possible token that can represent the handler.
        //
        // This means that both:
        //  * if there is a handler assigned to the generated initial token value, it is not necessarily
        //    this handler.
        //  * if there is no handler assigned to the generated initial token value, the handler may still
        //    be registered under a different token
        //
        // Effectively the only reasonable thing to do with this value is either to:
        //  1. Use it as a good starting point for generating a token for handler
        //  2. Use it as a guess to quickly see if the handler was really assigned this token value
        private static EventRegistrationToken GetPreferredToken(T handler)
        {
            Contract.Requires(handler != null);

            // We want to generate a token value that has the following properties:
            //  1. is quickly obtained from the handler instance
            //  2. uses bits in the upper 32 bits of the 64 bit value, in order to avoid bugs where code
            //     may assume the value is realy just 32 bits
            //  3. uses bits in the bottom 32 bits of the 64 bit value, in order to ensure that code doesn't
            //     take a dependency on them always being 0.
            //
            // The simple algorithm chosen here is to simply assign the upper 32 bits the metadata token of the
            // event handler type, and the lower 32 bits the hash code of the handler instance itself. Using the
            // metadata token for the upper 32 bits gives us at least a small chance of being able to identify a
            // totally corrupted token if we ever come across one in a minidump or other scenario.
            //
            // The hash code of a unicast delegate is not tied to the method being invoked, so in the case
            // of a unicast delegate, the hash code of the target method is used instead of the full delegate
            // hash code.
            //
            // While calculating this initial value will be somewhat more expensive than just using a counter
            // for events that have few registrations, it will also gives us a shot at preventing unregistration
            // from becoming an O(N) operation.
            //
            // We should feel free to change this algorithm as other requirements / optimizations become
            // available.  This implementation is sufficiently random that code cannot simply guess the value to
            // take a dependency upon it.  (Simply applying the hash-value algorithm directly won't work in the
            // case of collisions, where we'll use a different token value).

            uint handlerHashCode = 0;
            Delegate[] invocationList = ((Delegate)(object)handler).GetInvocationList();
            if (invocationList.Length == 1)
            {
                handlerHashCode = (uint)invocationList[0].Method.GetHashCode();
            }
            else
            {
                handlerHashCode = (uint)handler.GetHashCode();
            }

            ulong tokenValue = ((ulong)(uint)typeof(T).MetadataToken << 32) | handlerHashCode;
            return new EventRegistrationToken(tokenValue);
        }

        public void RemoveEventHandler(EventRegistrationToken token)
        {
            // The 0 token is assigned to null handlers, so there's nothing to do
            if (token.Value == 0)
            {
                return;
            }

            lock (m_tokens)
            {
                RemoveEventHandlerNoLock(token);
            }
        }

        public void RemoveEventHandler(T handler)
        {
            // To match the Windows Runtime behaivor when adding a null handler, removing one is a no-op
            if (handler == null)
            {
                return;
            }

            lock (m_tokens)
            {
                // Fast path - if the delegate is stored with its preferred token, then there's no need to do
                // a full search of the table for it.  Note that even if we find something stored using the
                // preferred token value, it's possible we have a collision and another delegate was using that
                // value.  Therefore we need to make sure we really have the handler we want before taking the
                // fast path.
                EventRegistrationToken preferredToken = GetPreferredToken(handler);
                T registeredHandler;
                if (m_tokens.TryGetValue(preferredToken, out registeredHandler))
                {
                    if (registeredHandler == handler)
                    {
                        RemoveEventHandlerNoLock(preferredToken);
                        return;
                    }
                }

                // Slow path - we didn't find the delegate with its preferred token, so we need to fall
                // back to a search of the table
                foreach (KeyValuePair<EventRegistrationToken, T> registration in m_tokens)
                {
                    if (registration.Value == (T)(object)handler)
                    {
                        RemoveEventHandlerNoLock(registration.Key);

                        // If a delegate has been added multiple times to handle an event, then it
                        // needs to be removed the same number of times to stop handling the event.
                        // Stop after the first one we find.
                        return;
                    }
                }

                // Note that falling off the end of the loop is not an error, as removing a registration
                // for a handler that is not currently registered is simply a no-op
            }
        }

        private void RemoveEventHandlerNoLock(EventRegistrationToken token)
        {
            T handler;
            if (m_tokens.TryGetValue(token, out handler))
            {
                m_tokens.Remove(token);

                // Update the current invocation list to remove the delegate
                Delegate invokeList = (Delegate)(object)m_invokeList;
                invokeList = MulticastDelegate.Remove(invokeList, (Delegate)(object)handler);
                m_invokeList = (T)(object)invokeList;
            }
        }

        public static EventRegistrationTokenTable<T> GetOrCreateEventRegistrationTokenTable(ref EventRegistrationTokenTable<T> refEventTable)
        {
            if (refEventTable == null)
            {
                Interlocked.CompareExchange(ref refEventTable, new EventRegistrationTokenTable<T>(), null);
            }
            return refEventTable;
        }
    }
}