1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Threading;
10 namespace System.Runtime.InteropServices.WindowsRuntime
12 // An event registration token table stores mappings from delegates to event tokens, in order to support
13 // sourcing WinRT style events from managed code.
14 public sealed class EventRegistrationTokenTable<T> where T : class
16 // Note this dictionary is also used as the synchronization object for this table
17 private Dictionary<EventRegistrationToken, T> m_tokens = new Dictionary<EventRegistrationToken, T>();
19 // Cached multicast delegate which will invoke all of the currently registered delegates. This
20 // will be accessed frequently in common coding paterns, so we don't want to calculate it repeatedly.
21 private volatile T m_invokeList = null!; // TODO-NULLABLE-GENERIC
23 public EventRegistrationTokenTable()
25 // T must be a delegate type, but we cannot constrain on being a delegate. Therefore, we'll do a
26 // static check at construction time
27 if (!typeof(Delegate).IsAssignableFrom(typeof(T)))
29 throw new InvalidOperationException(SR.Format(SR.InvalidOperation_EventTokenTableRequiresDelegate, typeof (T)));
33 // The InvocationList property provides access to a delegate which will invoke every registered event handler
34 // in this table. If the property is set, the new value will replace any existing token registrations.
35 public T InvocationList
46 // The value being set replaces any of the existing values
48 m_invokeList = null!; // TODO-NULLABLE-GENERIC
52 AddEventHandlerNoLock(value);
58 public EventRegistrationToken AddEventHandler(T handler)
60 // Windows Runtime allows null handlers. Assign those a token value of 0 for easy identity
63 return new EventRegistrationToken(0);
68 return AddEventHandlerNoLock(handler);
72 private EventRegistrationToken AddEventHandlerNoLock(T handler)
74 Debug.Assert(handler != null);
76 // Get a registration token, making sure that we haven't already used the value. This should be quite
77 // rare, but in the case it does happen, just keep trying until we find one that's unused.
78 EventRegistrationToken token = GetPreferredToken(handler);
79 while (m_tokens.ContainsKey(token))
81 token = new EventRegistrationToken(token.Value + 1);
83 m_tokens[token] = handler;
85 // Update the current invocation list to include the newly added delegate
86 Delegate? invokeList = (Delegate?)(object?)m_invokeList;
87 invokeList = MulticastDelegate.Combine(invokeList, (Delegate)(object)handler);
88 m_invokeList = (T)(object?)invokeList!; // TODO-NULLABLE-GENERIC
93 // Generate a token that may be used for a particular event handler. We will frequently be called
94 // upon to look up a token value given only a delegate to start from. Therefore, we want to make
95 // an initial token value that is easily determined using only the delegate instance itself. Although
96 // in the common case this token value will be used to uniquely identify the handler, it is not
97 // the only possible token that can represent the handler.
99 // This means that both:
100 // * if there is a handler assigned to the generated initial token value, it is not necessarily
102 // * if there is no handler assigned to the generated initial token value, the handler may still
103 // be registered under a different token
105 // Effectively the only reasonable thing to do with this value is either to:
106 // 1. Use it as a good starting point for generating a token for handler
107 // 2. Use it as a guess to quickly see if the handler was really assigned this token value
108 private static EventRegistrationToken GetPreferredToken(T handler)
110 Debug.Assert(handler != null);
112 // We want to generate a token value that has the following properties:
113 // 1. is quickly obtained from the handler instance
114 // 2. uses bits in the upper 32 bits of the 64 bit value, in order to avoid bugs where code
115 // may assume the value is realy just 32 bits
116 // 3. uses bits in the bottom 32 bits of the 64 bit value, in order to ensure that code doesn't
117 // take a dependency on them always being 0.
119 // The simple algorithm chosen here is to simply assign the upper 32 bits the metadata token of the
120 // event handler type, and the lower 32 bits the hash code of the handler instance itself. Using the
121 // metadata token for the upper 32 bits gives us at least a small chance of being able to identify a
122 // totally corrupted token if we ever come across one in a minidump or other scenario.
124 // The hash code of a unicast delegate is not tied to the method being invoked, so in the case
125 // of a unicast delegate, the hash code of the target method is used instead of the full delegate
128 // While calculating this initial value will be somewhat more expensive than just using a counter
129 // for events that have few registrations, it will also gives us a shot at preventing unregistration
130 // from becoming an O(N) operation.
132 // We should feel free to change this algorithm as other requirements / optimizations become
133 // available. This implementation is sufficiently random that code cannot simply guess the value to
134 // take a dependency upon it. (Simply applying the hash-value algorithm directly won't work in the
135 // case of collisions, where we'll use a different token value).
137 uint handlerHashCode = 0;
138 Delegate[] invocationList = ((Delegate)(object)handler).GetInvocationList();
139 if (invocationList.Length == 1)
141 handlerHashCode = (uint)invocationList[0].Method.GetHashCode();
145 handlerHashCode = (uint)handler.GetHashCode();
148 ulong tokenValue = ((ulong)(uint)typeof(T).MetadataToken << 32) | handlerHashCode;
149 return new EventRegistrationToken(tokenValue);
152 // Remove the event handler from the table and
153 // Get the delegate associated with an event registration token if it exists
154 // If the event registration token is not registered, returns false
155 public bool RemoveEventHandler(EventRegistrationToken token, out T handler)
159 if (m_tokens.TryGetValue(token, out handler))
161 RemoveEventHandlerNoLock(token);
169 public void RemoveEventHandler(EventRegistrationToken token)
171 // The 0 token is assigned to null handlers, so there's nothing to do
172 if (token.Value == 0)
179 RemoveEventHandlerNoLock(token);
183 public void RemoveEventHandler(T handler)
185 // To match the Windows Runtime behaivor when adding a null handler, removing one is a no-op
193 // Fast path - if the delegate is stored with its preferred token, then there's no need to do
194 // a full search of the table for it. Note that even if we find something stored using the
195 // preferred token value, it's possible we have a collision and another delegate was using that
196 // value. Therefore we need to make sure we really have the handler we want before taking the
198 EventRegistrationToken preferredToken = GetPreferredToken(handler);
200 if (m_tokens.TryGetValue(preferredToken, out registeredHandler))
202 if (registeredHandler == handler)
204 RemoveEventHandlerNoLock(preferredToken);
209 // Slow path - we didn't find the delegate with its preferred token, so we need to fall
210 // back to a search of the table
211 foreach (KeyValuePair<EventRegistrationToken, T> registration in m_tokens)
213 if (registration.Value == (T)(object)handler)
215 RemoveEventHandlerNoLock(registration.Key);
217 // If a delegate has been added multiple times to handle an event, then it
218 // needs to be removed the same number of times to stop handling the event.
219 // Stop after the first one we find.
224 // Note that falling off the end of the loop is not an error, as removing a registration
225 // for a handler that is not currently registered is simply a no-op
229 private void RemoveEventHandlerNoLock(EventRegistrationToken token)
232 if (m_tokens.TryGetValue(token, out handler))
234 m_tokens.Remove(token);
236 // Update the current invocation list to remove the delegate
237 Delegate? invokeList = (Delegate?)(object?)m_invokeList;
238 invokeList = MulticastDelegate.Remove(invokeList, (Delegate?)(object?)handler);
239 m_invokeList = (T)(object?)invokeList!; // TODO-NULLABLE-GENERIC
243 public static EventRegistrationTokenTable<T> GetOrCreateEventRegistrationTokenTable(ref EventRegistrationTokenTable<T>? refEventTable)
245 if (refEventTable == null)
247 Interlocked.CompareExchange(ref refEventTable, new EventRegistrationTokenTable<T>(), null);
249 return refEventTable!; // TODO-NULLABLE: https://github.com/dotnet/roslyn/issues/26761