// 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.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using CommunityToolkit.Mvvm.Messaging.Internals; namespace CommunityToolkit.Mvvm.Messaging; /// /// A class providing a reference implementation for the interface. /// /// /// /// This implementation uses weak references to track the registered /// recipients, so it is not necessary to manually unregister them when they're no longer needed. /// /// /// The type will automatically perform internal trimming when /// full GC collections are invoked, so calling manually is not necessary to /// ensure that on average the internal data structures are as trimmed and compact as possible. /// /// public sealed class WeakReferenceMessenger : IMessenger { // This messenger uses the following logic to link stored instances together: // -------------------------------------------------------------------------------------------------------- // Dictionary2 mapping // / / / // ___(Type2.TToken)___/ / / ___(if Type2.TToken is Unit) // /_________(Type2.TMessage)______________/ / / // / _________________/___MessageHandlerDispatcher? // / / \ // Dictionary2> recipientsMap; \___(null if using IRecipient) // -------------------------------------------------------------------------------------------------------- // Just like in the strong reference variant, each pair of message and token types is used as a key in the // recipients map. In this case, the values in the dictionary are ConditionalWeakTable2<,> instances, that // link each registered recipient to a map of currently registered handlers, through dependent handles. This // ensures that handlers will remain alive as long as their associated recipient is also alive (so there is no // need for users to manually indicate whether a given handler should be kept alive in case it creates a closure). // The value in each conditional table can either be Dictionary2 or object. The // first case is used when any token type other than the default Unit type is used, as in this case there could be // multiple handlers for each recipient that need to be tracked separately. In order to invoke all the handlers from // a context where their type parameters is not known, handlers are stored as MessageHandlerDispatcher instances. There // are two possible cases here: either a given instance is of type MessageHandlerDispatcher.For, // or null. The first is the default case: whenever a subscription is done with a MessageHandler, // that delegate is wrapped in an instance of this class so that it can keep track internally of the generic context in // use, so that it can be retrieved when the callback is executed. If the subscription is done directly on a recipient // that implements IRecipient /// The map of currently registered recipients for all message types. /// private readonly Dictionary2> recipientsMap = new(); /// /// Initializes a new instance of the class. /// public WeakReferenceMessenger() { // Proxy function for the GC callback. This needs to be static and to take the target instance as // an input parameter in order to avoid rooting it from the Gen2GcCallback object invoking it. static void Gen2GcCallbackProxy(object target) { ((WeakReferenceMessenger)target).CleanupWithNonBlockingLock(); } // Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the // current messenger instance is trimmed and without leftover recipient maps that are no longer used. // This is necessary (as in, some form of cleanup, either explicit or automatic like in this case) // because the ConditionalWeakTable instances will just remove key-value pairs on their // own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the Type2 instances // mapping to each conditional table for a pair of message and token types) to potentially remain in the // root mapping structure but without any remaining recipients actually registered there, which just // adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on. Gen2GcCallback.Register(Gen2GcCallbackProxy, this); } /// /// Gets the default instance. /// public static WeakReferenceMessenger Default { get; } = new(); /// public bool IsRegistered(object recipient, TToken token) where TMessage : class where TToken : IEquatable { ArgumentNullException.ThrowIfNull(recipient); ArgumentNullException.For.ThrowIfNull(token); lock (this.recipientsMap) { Type2 type2 = new(typeof(TMessage), typeof(TToken)); // Get the conditional table associated with the target recipient, for the current pair // of token and message types. If it exists, check if there is a matching token. if (!this.recipientsMap.TryGetValue(type2, out ConditionalWeakTable2? table)) { return false; } // Special case for unit tokens if (typeof(TToken) == typeof(Unit)) { return table.TryGetValue(recipient, out _); } // Custom token type, so each recipient has an associated map return table.TryGetValue(recipient, out object? mapping) && Unsafe.As>(mapping!).ContainsKey(token); } } /// public void Register(TRecipient recipient, TToken token, MessageHandler handler) where TRecipient : class where TMessage : class where TToken : IEquatable { ArgumentNullException.ThrowIfNull(recipient); ArgumentNullException.For.ThrowIfNull(token); ArgumentNullException.ThrowIfNull(handler); Register(recipient, token, new MessageHandlerDispatcher.For(handler)); } /// /// Registers a recipient for a given type of message. /// /// The type of message to receive. /// The type of token to use to pick the messages to receive. /// The recipient that will receive the messages. /// A token used to determine the receiving channel to use. /// Thrown when trying to register the same message twice. /// /// This method is a variation of /// that is specialized for recipients implementing . See more comments at the top of this type, as well as /// within and in the types. /// internal void Register(IRecipient recipient, TToken token) where TMessage : class where TToken : IEquatable { Register(recipient, token, null); } /// /// Registers a recipient for a given type of message. /// /// The type of message to receive. /// The type of token to use to pick the messages to receive. /// The recipient that will receive the messages. /// A token used to determine the receiving channel to use. /// The input instance to register, or null. /// Thrown when trying to register the same message twice. private void Register(object recipient, TToken token, MessageHandlerDispatcher? dispatcher) where TMessage : class where TToken : IEquatable { lock (this.recipientsMap) { Type2 type2 = new(typeof(TMessage), typeof(TToken)); // Get the conditional table for the pair of type arguments, or create it if it doesn't exist ref ConditionalWeakTable2? mapping = ref this.recipientsMap.GetOrAddValueRef(type2); mapping ??= new ConditionalWeakTable2(); // Fast path for unit tokens if (typeof(TToken) == typeof(Unit)) { if (!mapping.TryAdd(recipient, dispatcher)) { ThrowInvalidOperationExceptionForDuplicateRegistration(); } } else { // Get or create the handlers dictionary for the target recipient Dictionary2? map = Unsafe.As>(mapping.GetValue(recipient, static _ => new Dictionary2())!); // Add the new registration entry ref object? registeredHandler = ref map.GetOrAddValueRef(token); if (registeredHandler is not null) { ThrowInvalidOperationExceptionForDuplicateRegistration(); } // Store the input handler registeredHandler = dispatcher; } } } /// public void UnregisterAll(object recipient) { ArgumentNullException.ThrowIfNull(recipient); lock (this.recipientsMap) { Dictionary2>.Enumerator enumerator = this.recipientsMap.GetEnumerator(); // Traverse all the existing conditional tables and remove all the ones // with the target recipient as key. We don't perform a cleanup here, // as that is responsibility of a separate method defined below. while (enumerator.MoveNext()) { _ = enumerator.GetValue().Remove(recipient); } } } /// public void UnregisterAll(object recipient, TToken token) where TToken : IEquatable { ArgumentNullException.ThrowIfNull(recipient); ArgumentNullException.For.ThrowIfNull(token); // This method is never called with the unit type. See more details in // the comments in the corresponding method in StrongReferenceMessenger. if (typeof(TToken) == typeof(Unit)) { throw new NotImplementedException(); } lock (this.recipientsMap) { Dictionary2>.Enumerator enumerator = this.recipientsMap.GetEnumerator(); // Same as above, with the difference being that this time we only go through // the conditional tables having a matching token type as key, and that we // only try to remove handlers with a matching token, if any. while (enumerator.MoveNext()) { if (enumerator.GetKey().TToken == typeof(TToken)) { if (enumerator.GetValue().TryGetValue(recipient, out object? mapping)) { _ = Unsafe.As>(mapping!).TryRemove(token); } } } } } /// public void Unregister(object recipient, TToken token) where TMessage : class where TToken : IEquatable { ArgumentNullException.ThrowIfNull(recipient); ArgumentNullException.For.ThrowIfNull(token); lock (this.recipientsMap) { Type2 type2 = new(typeof(TMessage), typeof(TToken)); // Get the target mapping table for the combination of message and token types, // and remove the handler with a matching token (the entire map), if present. if (this.recipientsMap.TryGetValue(type2, out ConditionalWeakTable2? value)) { if (typeof(TToken) == typeof(Unit)) { _ = value.Remove(recipient); } else if (value.TryGetValue(recipient, out object? mapping)) { _ = Unsafe.As>(mapping!).TryRemove(token); } } } } /// public TMessage Send(TMessage message, TToken token) where TMessage : class where TToken : IEquatable { ArgumentNullException.ThrowIfNull(message); ArgumentNullException.For.ThrowIfNull(token); ArrayPoolBufferWriter bufferWriter; int i = 0; lock (this.recipientsMap) { Type2 type2 = new(typeof(TMessage), typeof(TToken)); // Try to get the target table if (!this.recipientsMap.TryGetValue(type2, out ConditionalWeakTable2? table)) { return message; } bufferWriter = ArrayPoolBufferWriter.Create(); // We need a local, temporary copy of all the pending recipients and handlers to // invoke, to avoid issues with handlers unregistering from messages while we're // holding the lock. To do this, we can just traverse the conditional table in use // to enumerate all the existing recipients for the token and message types pair // corresponding to the generic arguments for this invocation, and then track the // handlers with a matching token, and their corresponding recipients. using ConditionalWeakTable2.Enumerator enumerator = table.GetEnumerator(); while (enumerator.MoveNext()) { if (typeof(TToken) == typeof(Unit)) { bufferWriter.Add(enumerator.GetValue()); bufferWriter.Add(enumerator.GetKey()); i++; } else { Dictionary2? map = Unsafe.As>(enumerator.GetValue()!); if (map.TryGetValue(token, out object? handler)) { bufferWriter.Add(handler); bufferWriter.Add(enumerator.GetKey()); i++; } } } } try { SendAll(bufferWriter.Span, i, message); } finally { bufferWriter.Dispose(); } return message; } /// /// Implements the broadcasting logic for . /// /// /// /// /// /// /// This method is not a local function to avoid triggering multiple compilations due to TToken /// potentially being a value type, which results in specialized code due to reified generics. This is /// necessary to work around a Roslyn limitation that causes unnecessary type parameters in local /// functions not to be discarded in the synthesized methods. Additionally, keeping this loop outside /// of the EH block (the block) can help result in slightly better codegen. /// [MethodImpl(MethodImplOptions.NoInlining)] internal static void SendAll(ReadOnlySpan pairs, int i, TMessage message) where TMessage : class { // This Slice calls executes bounds checks for the loop below, in case i was somehow wrong. // The rest of the implementation relies on bounds checks removal and loop strength reduction // done manually (which results in a 20% speedup during broadcast), since the JIT is not able // to recognize this pattern. Skipping checks below is a provably safe optimization: the slice // has exactly 2 * i elements (due to this slicing), and each loop iteration processes a pair. // The loops ends when the initial reference reaches the end, and that's incremented by 2 at // the end of each iteration. The target being a span, obviously means the length is constant. ReadOnlySpan slice = pairs.Slice(0, 2 * i); ref object? sliceStart = ref MemoryMarshal.GetReference(slice); ref object? sliceEnd = ref Unsafe.Add(ref sliceStart, slice.Length); while (Unsafe.IsAddressLessThan(ref sliceStart, ref sliceEnd)) { object? handler = sliceStart; object recipient = Unsafe.Add(ref sliceStart, 1)!; // Here we need to distinguish the two possible cases: either the recipient was registered // through the IRecipient interface, or with a custom handler. In the first case, // the handler stored in the messenger is just null, so we can check that and branch to a // fast path that just invokes IRecipient directly on the recipient. Otherwise, // we will use the standard double dispatch approach. This check is particularly convenient // as we only need to check for null to determine what registration type was used, without // having to store any additional info in the messenger. This will produce code as follows, // with the advantage of also being compact and not having to use any additional registers: // ============================= // L0000: test rcx, rcx // L0003: jne short L0040 // ============================= // Which is extremely fast. The reason for this conditional check in the first place is that // we're doing manual (null based) guarded devirtualization: if the handler is the marker // type and not an actual handler then we know that the recipient implements // IRecipient, so we can just cast to it and invoke it directly. This avoids // having to store the proxy callback when registering, and also skips an indirection // (invoking the delegate that then invokes the actual method). Additional note: this // pattern ensures that both casts below do not actually alias incompatible reference // types (as in, they would both succeed if they were safe casts), which lets the code // not rely on undefined behavior to run correctly (ie. we're not aliasing delegates). if (handler is null) { Unsafe.As>(recipient).Receive(message); } else { Unsafe.As(handler).Invoke(recipient, message); } sliceStart = ref Unsafe.Add(ref sliceStart, 2); } } /// public void Cleanup() { lock (this.recipientsMap) { CleanupWithoutLock(); } } /// public void Reset() { lock (this.recipientsMap) { this.recipientsMap.Clear(); } } /// /// Executes a cleanup without locking the current instance. This method has to be /// invoked when a lock on has already been acquired. /// private void CleanupWithNonBlockingLock() { object lockObject = this.recipientsMap; bool lockTaken = false; try { Monitor.TryEnter(lockObject, ref lockTaken); if (lockTaken) { CleanupWithoutLock(); } } finally { if (lockTaken) { Monitor.Exit(lockObject); } } } /// /// Executes a cleanup without locking the current instance. This method has to be /// invoked when a lock on has already been acquired. /// private void CleanupWithoutLock() { using ArrayPoolBufferWriter type2s = ArrayPoolBufferWriter.Create(); using ArrayPoolBufferWriter emptyRecipients = ArrayPoolBufferWriter.Create(); Dictionary2>.Enumerator type2Enumerator = this.recipientsMap.GetEnumerator(); // First, we go through all the currently registered pairs of token and message types. // These represents all the combinations of generic arguments with at least one registered // handler, with the exception of those with recipients that have already been collected. while (type2Enumerator.MoveNext()) { emptyRecipients.Reset(); bool hasAtLeastOneHandler = false; if (type2Enumerator.GetKey().TToken == typeof(Unit)) { // When the token type is unit, there can be no registered recipients with no handlers, // as when the single handler is unsubscribed the recipient is also removed immediately. // Therefore, we need to check that there exists at least one recipient for the message. using ConditionalWeakTable2.Enumerator recipientsEnumerator = type2Enumerator.GetValue().GetEnumerator(); while (recipientsEnumerator.MoveNext()) { hasAtLeastOneHandler = true; break; } } else { // Go through the currently alive recipients to look for those with no handlers left. We track // the ones we find to remove them outside of the loop (can't modify during enumeration). using (ConditionalWeakTable2.Enumerator recipientsEnumerator = type2Enumerator.GetValue().GetEnumerator()) { while (recipientsEnumerator.MoveNext()) { if (Unsafe.As(recipientsEnumerator.GetValue()!).Count == 0) { emptyRecipients.Add(recipientsEnumerator.GetKey()); } else { hasAtLeastOneHandler = true; } } } // Remove the handler maps for recipients that are still alive but with no handlers foreach (object recipient in emptyRecipients.Span) { _ = type2Enumerator.GetValue().Remove(recipient); } } // Track the type combinations with no recipients or handlers left if (!hasAtLeastOneHandler) { type2s.Add(type2Enumerator.GetKey()); } } // Remove all the mappings with no handlers left foreach (Type2 key in type2s.Span) { _ = this.recipientsMap.TryRemove(key); } } /// /// Throws an when trying to add a duplicate handler. /// private static void ThrowInvalidOperationExceptionForDuplicateRegistration() { throw new InvalidOperationException("The target recipient has already subscribed to the target message."); } }