Nullable: System.Runtime.InteropServices.CustomMarshalers/WindowsRuntime (#23930)
[platform/upstream/coreclr.git] / src / System.Private.CoreLib / src / System / Runtime / InteropServices / WindowsRuntime / EventRegistrationTokenTable.cs
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.
4
5 #nullable enable
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Threading;
9
10 namespace System.Runtime.InteropServices.WindowsRuntime
11 {
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
15     {
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>();
18
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
22
23         public EventRegistrationTokenTable()
24         {
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)))
28             {
29                 throw new InvalidOperationException(SR.Format(SR.InvalidOperation_EventTokenTableRequiresDelegate, typeof (T)));
30             }
31         }
32
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
36         {
37             get
38             {
39                 return m_invokeList;
40             }
41
42             set
43             {
44                 lock (m_tokens)
45                 {
46                     // The value being set replaces any of the existing values
47                     m_tokens.Clear();
48                     m_invokeList = null!; // TODO-NULLABLE-GENERIC
49
50                     if (value != null)
51                     {
52                         AddEventHandlerNoLock(value);
53                     }
54                 }
55             }
56         }
57
58         public EventRegistrationToken AddEventHandler(T handler)
59         {
60             // Windows Runtime allows null handlers.  Assign those a token value of 0 for easy identity
61             if (handler == null)
62             {
63                 return new EventRegistrationToken(0);
64             }
65
66             lock (m_tokens)
67             {
68                 return AddEventHandlerNoLock(handler);
69             }
70         }
71
72         private EventRegistrationToken AddEventHandlerNoLock(T handler)
73         {
74             Debug.Assert(handler != null);
75
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))
80             {
81                 token = new EventRegistrationToken(token.Value + 1);
82             }
83             m_tokens[token] = handler;
84
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
89
90             return token;
91         }
92
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.
98         //
99         // This means that both:
100         //  * if there is a handler assigned to the generated initial token value, it is not necessarily
101         //    this handler.
102         //  * if there is no handler assigned to the generated initial token value, the handler may still
103         //    be registered under a different token
104         //
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)
109         {
110             Debug.Assert(handler != null);
111
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.
118             //
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.
123             //
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
126             // hash code.
127             //
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.
131             //
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).
136
137             uint handlerHashCode = 0;
138             Delegate[] invocationList = ((Delegate)(object)handler).GetInvocationList();
139             if (invocationList.Length == 1)
140             {
141                 handlerHashCode = (uint)invocationList[0].Method.GetHashCode();
142             }
143             else
144             {
145                 handlerHashCode = (uint)handler.GetHashCode();
146             }
147
148             ulong tokenValue = ((ulong)(uint)typeof(T).MetadataToken << 32) | handlerHashCode;
149             return new EventRegistrationToken(tokenValue);
150         }
151
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)
156         {
157             lock (m_tokens)
158             {
159                 if (m_tokens.TryGetValue(token, out handler))
160                 {
161                     RemoveEventHandlerNoLock(token);
162                     return true;
163                 }
164             }
165
166             return false;
167         }
168
169         public void RemoveEventHandler(EventRegistrationToken token)
170         {
171             // The 0 token is assigned to null handlers, so there's nothing to do
172             if (token.Value == 0)
173             {
174                 return;
175             }
176
177             lock (m_tokens)
178             {
179                 RemoveEventHandlerNoLock(token);
180             }
181         }
182
183         public void RemoveEventHandler(T handler)
184         {
185             // To match the Windows Runtime behaivor when adding a null handler, removing one is a no-op
186             if (handler == null)
187             {
188                 return;
189             }
190
191             lock (m_tokens)
192             {
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
197                 // fast path.
198                 EventRegistrationToken preferredToken = GetPreferredToken(handler);
199                 T registeredHandler;
200                 if (m_tokens.TryGetValue(preferredToken, out registeredHandler))
201                 {
202                     if (registeredHandler == handler)
203                     {
204                         RemoveEventHandlerNoLock(preferredToken);
205                         return;
206                     }
207                 }
208
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)
212                 {
213                     if (registration.Value == (T)(object)handler)
214                     {
215                         RemoveEventHandlerNoLock(registration.Key);
216
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.
220                         return;
221                     }
222                 }
223
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
226             }
227         }
228
229         private void RemoveEventHandlerNoLock(EventRegistrationToken token)
230         {
231             T handler;
232             if (m_tokens.TryGetValue(token, out handler))
233             {
234                 m_tokens.Remove(token);
235
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
240             }
241         }
242
243         public static EventRegistrationTokenTable<T> GetOrCreateEventRegistrationTokenTable(ref EventRegistrationTokenTable<T>? refEventTable)
244         {
245             if (refEventTable == null)
246             {
247                 Interlocked.CompareExchange(ref refEventTable, new EventRegistrationTokenTable<T>(), null);
248             }
249             return refEventTable!; // TODO-NULLABLE: https://github.com/dotnet/roslyn/issues/26761
250         }
251     }
252 }