From 4342454a03231c3b6e0a7bf6f0d55483e241981f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 15:20:06 +0200 Subject: [PATCH 01/13] Added DispatcherQueue.TryEnqueue extensions Fixes https://github.com/microsoft/microsoft-ui-xaml/issues/3321 --- .../CommunityToolkit.WinUI.csproj | 1 + .../Extensions/DispatcherQueueExtensions.cs | 2 +- .../DispatcherQueueExtensions{T}.cs | 305 ++++++++++++++++++ 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs diff --git a/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj b/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj index ab0ab1e310c..6cc21e4eea6 100644 --- a/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj +++ b/CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj @@ -8,6 +8,7 @@ Windows Community Toolkit - Common (UWP) This package includes code only helpers such as Color conversion tool, Storage file handling, a Stream helper class, SystemInformation helpers, etc. Storage;File;Folder;Color;Conversion;Stream;Helpers;Extensions;System;Information + true diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs index a2f172a0024..a5e88985ece 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.WinUI /// /// Helpers for executing code in a . /// - public static class DispatcherQueueExtensions + public static partial class DispatcherQueueExtensions { /// /// Invokes a given function on the target and returns a diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs new file mode 100644 index 00000000000..7579f7ff935 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -0,0 +1,305 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.UI.Dispatching; +using WinRT; + +#nullable enable + +#pragma warning disable SA1000, SA1023 + +namespace CommunityToolkit.WinUI +{ + /// + /// A callback that will be executed on the thread. + /// + /// The type of state to receive as input. + /// The input state for the callback. + public delegate void DispatcherQueueHandler(TState state) + where TState : class; + + /// + /// Helpers for executing code in a . + /// + public static partial class DispatcherQueueExtensions + { + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of state to capture. + /// The target to invoke the code on. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, TState state) + where TState : class + { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); + + byte result; + + try + { + _ = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, &result); + + GC.KeepAlive(dispatcherQueue); + } + finally + { + dispatcherQueueHandlerPtr->Release(); + } + + return result == 0; + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of state to capture. + /// The target to invoke the code on. + /// The desired priority for the callback to schedule. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, TState state) + where TState : class + { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); + + byte result; + + try + { + _ = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, &result); + + GC.KeepAlive(dispatcherQueue); + } + finally + { + dispatcherQueueHandlerPtr->Release(); + } + + return result == 0; + } + + /// + /// A struct mapping the native WinRT IDispatcherQueue interface. + /// + private unsafe struct IDispatcherQueue + { + private readonly void** lpVtbl; + + /// + /// Native API for . + /// + /// A pointer to an IDispatcherQueueHandler object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueue(void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); + } + + /// + /// Native API for . + /// + /// The priority for the input callback. + /// A pointer to an IDispatcherQueueHandler object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); + } + } + + /// + /// A custom IDispatcherQueueHandler object, that internally stores a captured instance + /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. + /// + private unsafe struct DispatcherQueueProxyHandler + { + private const int S_OK = 0; + private const int E_NOINTERFACE = unchecked((int)0x80004002); + + private static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + private static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); + private static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); + + /// + /// The shared vtable pointer for instances. + /// + private static readonly void** Vtbl = InitVtbl(); + + /// + /// Setups the vtable pointer for . + /// + /// The initialized vtable pointer for . + /// + /// The vtable itself is allocated with , + /// which allocates memory in the high frequency heap associated with the input runtime type. This will be + /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. + /// + private static void** InitVtbl() + { + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler), sizeof(void*) * 4); + + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + + return lpVtbl; + } + + /// + /// The vtable pointer for the current instance. + /// + private void** lpVtbl; + + /// + /// The to the captured (for some unknown TState type). + /// + private GCHandle callbackHandle; + + /// + /// The to the captured state (with an unknown TState type). + /// + private GCHandle stateHandle; + + /// + /// The current reference count for the object (from IUnknown). + /// + private volatile uint referenceCount; + + /// + /// Creates a new instance for the input callback and state. + /// + /// The type of state to capture. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler* Create(DispatcherQueueHandler handler, TState state) + where TState : class + { + DispatcherQueueProxyHandler* @this = (DispatcherQueueProxyHandler*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler)); + + @this->lpVtbl = Vtbl; + @this->callbackHandle = GCHandle.Alloc(handler); + @this->stateHandle = GCHandle.Alloc(state); + @this->referenceCount = 1; + + return @this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AddRef() + { + return Interlocked.Increment(ref referenceCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + uint referenceCount = Interlocked.Decrement(ref this.referenceCount); + + if (referenceCount == 0) + { + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); + } + + return referenceCount; + } + + /// + /// A private type with the implementation of the unmanaged methods for . + /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. + /// + private static class Impl + { + /// + /// Implements IUnknown.QueryInterface(REFIID, void**). + /// + [UnmanagedCallersOnly] + public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, void** ppvObject) + { + if (riid->Equals(IUnknown) || + riid->Equals(IAgileObject) || + riid->Equals(IDispatcherQueueHandler)) + { + @this->AddRef(); + + *ppvObject = @this; + + return S_OK; + } + + return E_NOINTERFACE; + } + + /// + /// Implements IUnknown.AddRef(). + /// + [UnmanagedCallersOnly] + public static uint AddRef(DispatcherQueueProxyHandler* @this) + { + return Interlocked.Increment(ref @this->referenceCount); + } + + /// + /// Implements IUnknown.Release(). + /// + [UnmanagedCallersOnly] + public static uint Release(DispatcherQueueProxyHandler* @this) + { + uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); + + if (referenceCount == 0) + { + @this->callbackHandle.Free(); + @this->stateHandle.Free(); + + Marshal.FreeHGlobal((IntPtr)@this); + } + + return referenceCount; + } + + /// + /// Implements IDispatcherQueueHandler.Invoke(). + /// + [UnmanagedCallersOnly] + public static int Invoke(DispatcherQueueProxyHandler* @this) + { + object callback = @this->callbackHandle.Target!; + object state = @this->stateHandle.Target!; + + try + { + // We do an unsafe cast here to treat the captured delegate as if the contravariant + // input type was actually declared as covariant. This is valid because the type + // parameter is constrained to be a reference type, and due to how the proxy handler + // is constructed we know that the captured state will always match the actual type + // of the captured handler at this point. This lets this whole method work without the + // need to make the proxy type itself generic, so without knowing the actual type argument. + Unsafe.As>(callback)(state); + } + catch + { + } + + return S_OK; + } + } + } + } +} \ No newline at end of file From 687063012e7339e6922d8dfad0d62a7bd63caaf7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 15:28:02 +0200 Subject: [PATCH 02/13] Added exception handling for WinRT calls --- .../DispatcherQueueExtensions{T}.cs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index 7579f7ff935..3d0c885678f 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -36,17 +36,19 @@ public static partial class DispatcherQueueExtensions /// The input callback to enqueue. /// The input state to capture and pass to the callback. /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, TState state) where TState : class { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); - byte result; + bool success; + int hResult; try { - _ = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, &result); + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, (byte*)&success); GC.KeepAlive(dispatcherQueue); } @@ -55,7 +57,12 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu dispatcherQueueHandlerPtr->Release(); } - return result == 0; + if (hResult != 0) + { + ExceptionHelpers.ThrowExceptionForHR(hResult); + } + + return success; } /// @@ -67,17 +74,19 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// The input callback to enqueue. /// The input state to capture and pass to the callback. /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, TState state) where TState : class { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); - byte result; + bool success; + int hResult; try { - _ = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, &result); + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, (byte*)&success); GC.KeepAlive(dispatcherQueue); } @@ -86,7 +95,12 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu dispatcherQueueHandlerPtr->Release(); } - return result == 0; + if (hResult != 0) + { + ExceptionHelpers.ThrowExceptionForHR(hResult); + } + + return success; } /// From 332f96e2789cd3088d9151dd25ef6843939264de Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 15:59:50 +0200 Subject: [PATCH 03/13] Minor code refactoring --- .../DispatcherQueueExtensions{T}.cs | 219 +----------------- .../Interop/DispatcherQueueProxyHandler.cs | 195 ++++++++++++++++ .../Extensions/Interop/IDispatcherQueue.cs | 46 ++++ 3 files changed, 242 insertions(+), 218 deletions(-) create mode 100644 CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs create mode 100644 CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index 3d0c885678f..993cb7bb81d 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -3,16 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; +using CommunityToolkit.WinUI.Interop; using Microsoft.UI.Dispatching; using WinRT; #nullable enable -#pragma warning disable SA1000, SA1023 - namespace CommunityToolkit.WinUI { /// @@ -102,218 +98,5 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu return success; } - - /// - /// A struct mapping the native WinRT IDispatcherQueue interface. - /// - private unsafe struct IDispatcherQueue - { - private readonly void** lpVtbl; - - /// - /// Native API for . - /// - /// A pointer to an IDispatcherQueueHandler object. - /// The result of the operation (the WinRT retval). - /// The HRESULT for the operation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueue(void* callback, byte* result) - { - return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); - } - - /// - /// Native API for . - /// - /// The priority for the input callback. - /// A pointer to an IDispatcherQueueHandler object. - /// The result of the operation (the WinRT retval). - /// The HRESULT for the operation. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) - { - return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); - } - } - - /// - /// A custom IDispatcherQueueHandler object, that internally stores a captured instance - /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. - /// - private unsafe struct DispatcherQueueProxyHandler - { - private const int S_OK = 0; - private const int E_NOINTERFACE = unchecked((int)0x80004002); - - private static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); - private static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); - private static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); - - /// - /// The shared vtable pointer for instances. - /// - private static readonly void** Vtbl = InitVtbl(); - - /// - /// Setups the vtable pointer for . - /// - /// The initialized vtable pointer for . - /// - /// The vtable itself is allocated with , - /// which allocates memory in the high frequency heap associated with the input runtime type. This will be - /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. - /// - private static void** InitVtbl() - { - void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler), sizeof(void*) * 4); - - lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; - lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; - lpVtbl[2] = (delegate* unmanaged)&Impl.Release; - lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; - - return lpVtbl; - } - - /// - /// The vtable pointer for the current instance. - /// - private void** lpVtbl; - - /// - /// The to the captured (for some unknown TState type). - /// - private GCHandle callbackHandle; - - /// - /// The to the captured state (with an unknown TState type). - /// - private GCHandle stateHandle; - - /// - /// The current reference count for the object (from IUnknown). - /// - private volatile uint referenceCount; - - /// - /// Creates a new instance for the input callback and state. - /// - /// The type of state to capture. - /// The input callback to enqueue. - /// The input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler* Create(DispatcherQueueHandler handler, TState state) - where TState : class - { - DispatcherQueueProxyHandler* @this = (DispatcherQueueProxyHandler*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler)); - - @this->lpVtbl = Vtbl; - @this->callbackHandle = GCHandle.Alloc(handler); - @this->stateHandle = GCHandle.Alloc(state); - @this->referenceCount = 1; - - return @this; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint AddRef() - { - return Interlocked.Increment(ref referenceCount); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint Release() - { - uint referenceCount = Interlocked.Decrement(ref this.referenceCount); - - if (referenceCount == 0) - { - Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); - } - - return referenceCount; - } - - /// - /// A private type with the implementation of the unmanaged methods for . - /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. - /// - private static class Impl - { - /// - /// Implements IUnknown.QueryInterface(REFIID, void**). - /// - [UnmanagedCallersOnly] - public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, void** ppvObject) - { - if (riid->Equals(IUnknown) || - riid->Equals(IAgileObject) || - riid->Equals(IDispatcherQueueHandler)) - { - @this->AddRef(); - - *ppvObject = @this; - - return S_OK; - } - - return E_NOINTERFACE; - } - - /// - /// Implements IUnknown.AddRef(). - /// - [UnmanagedCallersOnly] - public static uint AddRef(DispatcherQueueProxyHandler* @this) - { - return Interlocked.Increment(ref @this->referenceCount); - } - - /// - /// Implements IUnknown.Release(). - /// - [UnmanagedCallersOnly] - public static uint Release(DispatcherQueueProxyHandler* @this) - { - uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); - - if (referenceCount == 0) - { - @this->callbackHandle.Free(); - @this->stateHandle.Free(); - - Marshal.FreeHGlobal((IntPtr)@this); - } - - return referenceCount; - } - - /// - /// Implements IDispatcherQueueHandler.Invoke(). - /// - [UnmanagedCallersOnly] - public static int Invoke(DispatcherQueueProxyHandler* @this) - { - object callback = @this->callbackHandle.Target!; - object state = @this->stateHandle.Target!; - - try - { - // We do an unsafe cast here to treat the captured delegate as if the contravariant - // input type was actually declared as covariant. This is valid because the type - // parameter is constrained to be a reference type, and due to how the proxy handler - // is constructed we know that the captured state will always match the actual type - // of the captured handler at this point. This lets this whole method work without the - // need to make the proxy type itself generic, so without knowing the actual type argument. - Unsafe.As>(callback)(state); - } - catch - { - } - - return S_OK; - } - } - } } } \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs new file mode 100644 index 00000000000..5a07d84b20f --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs @@ -0,0 +1,195 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +#nullable enable + +#pragma warning disable SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A custom IDispatcherQueueHandler object, that internally stores a captured instance + /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. + /// + internal unsafe struct DispatcherQueueProxyHandler + { + private const int S_OK = 0; + private const int E_NOINTERFACE = unchecked((int)0x80004002); + + private static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + private static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); + private static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); + + /// + /// The shared vtable pointer for instances. + /// + private static readonly void** Vtbl = InitVtbl(); + + /// + /// Setups the vtable pointer for . + /// + /// The initialized vtable pointer for . + /// + /// The vtable itself is allocated with , + /// which allocates memory in the high frequency heap associated with the input runtime type. This will be + /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. + /// + private static void** InitVtbl() + { + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler), sizeof(void*) * 4); + + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + + return lpVtbl; + } + + /// + /// The vtable pointer for the current instance. + /// + private void** lpVtbl; + + /// + /// The to the captured (for some unknown TState type). + /// + private GCHandle callbackHandle; + + /// + /// The to the captured state (with an unknown TState type). + /// + private GCHandle stateHandle; + + /// + /// The current reference count for the object (from IUnknown). + /// + private volatile uint referenceCount; + + /// + /// Creates a new instance for the input callback and state. + /// + /// The type of state to capture. + /// The input callback to enqueue. + /// The input state to capture and pass to the callback. + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler* Create(DispatcherQueueHandler handler, TState state) + where TState : class + { + DispatcherQueueProxyHandler* @this = (DispatcherQueueProxyHandler*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler)); + + @this->lpVtbl = Vtbl; + @this->callbackHandle = GCHandle.Alloc(handler); + @this->stateHandle = GCHandle.Alloc(state); + @this->referenceCount = 1; + + return @this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AddRef() + { + return Interlocked.Increment(ref referenceCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + uint referenceCount = Interlocked.Decrement(ref this.referenceCount); + + if (referenceCount == 0) + { + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); + } + + return referenceCount; + } + + /// + /// A private type with the implementation of the unmanaged methods for . + /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. + /// + private static class Impl + { + /// + /// Implements IUnknown.QueryInterface(REFIID, void**). + /// + [UnmanagedCallersOnly] + public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, void** ppvObject) + { + if (riid->Equals(IUnknown) || + riid->Equals(IAgileObject) || + riid->Equals(IDispatcherQueueHandler)) + { + @this->AddRef(); + + *ppvObject = @this; + + return S_OK; + } + + return E_NOINTERFACE; + } + + /// + /// Implements IUnknown.AddRef(). + /// + [UnmanagedCallersOnly] + public static uint AddRef(DispatcherQueueProxyHandler* @this) + { + return Interlocked.Increment(ref @this->referenceCount); + } + + /// + /// Implements IUnknown.Release(). + /// + [UnmanagedCallersOnly] + public static uint Release(DispatcherQueueProxyHandler* @this) + { + uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); + + if (referenceCount == 0) + { + @this->callbackHandle.Free(); + @this->stateHandle.Free(); + + Marshal.FreeHGlobal((IntPtr)@this); + } + + return referenceCount; + } + + /// + /// Implements IDispatcherQueueHandler.Invoke(). + /// + [UnmanagedCallersOnly] + public static int Invoke(DispatcherQueueProxyHandler* @this) + { + object callback = @this->callbackHandle.Target!; + object state = @this->stateHandle.Target!; + + try + { + // We do an unsafe cast here to treat the captured delegate as if the contravariant + // input type was actually declared as covariant. This is valid because the type + // parameter is constrained to be a reference type, and due to how the proxy handler + // is constructed we know that the captured state will always match the actual type + // of the captured handler at this point. This lets this whole method work without the + // need to make the proxy type itself generic, so without knowing the actual type argument. + Unsafe.As>(callback)(state); + } + catch + { + } + + return S_OK; + } + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs new file mode 100644 index 00000000000..2007d693f61 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -0,0 +1,46 @@ +// 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.Runtime.CompilerServices; +using Microsoft.UI.Dispatching; + +#nullable enable + +#pragma warning disable SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A struct mapping the native WinRT IDispatcherQueue interface. + /// + internal unsafe struct IDispatcherQueue + { + private readonly void** lpVtbl; + + /// + /// Native API for . + /// + /// A pointer to an IDispatcherQueueHandler object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueue(void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); + } + + /// + /// Native API for . + /// + /// The priority for the input callback. + /// A pointer to an IDispatcherQueueHandler object. + /// The result of the operation (the WinRT retval). + /// The HRESULT for the operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) + { + return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); + } + } +} \ No newline at end of file From 39fba58fd0ab4dd1a10b1c56e3378470c2293dec Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 16:04:45 +0200 Subject: [PATCH 04/13] More code refactoring --- .../Interop/DispatcherQueueProxyHandler.cs | 8 +--- .../Extensions/Interop/IDispatcherQueue.cs | 2 +- .../Extensions/Interop/Windows.cs | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 CommunityToolkit.WinUI/Extensions/Interop/Windows.cs diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs index 5a07d84b20f..1cfacf21d06 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using static CommunityToolkit.WinUI.Interop.Windows; #nullable enable @@ -19,13 +20,6 @@ namespace CommunityToolkit.WinUI.Interop /// internal unsafe struct DispatcherQueueProxyHandler { - private const int S_OK = 0; - private const int E_NOINTERFACE = unchecked((int)0x80004002); - - private static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); - private static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); - private static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); - /// /// The shared vtable pointer for instances. /// diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs index 2007d693f61..b95b9bcac38 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -7,7 +7,7 @@ #nullable enable -#pragma warning disable SA1023 +#pragma warning disable CS0649, SA1023 namespace CommunityToolkit.WinUI.Interop { diff --git a/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs new file mode 100644 index 00000000000..0ded78bd108 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs @@ -0,0 +1,39 @@ +// 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; + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A helper class with some shared constants from the Windows headers. + /// + internal static class Windows + { + /// + /// The HRESULT for a successful operation. + /// + public const int S_OK = 0; + + /// + /// The HRESULT for an invalid cast from IUnknown.QueryInterface. + /// + public const int E_NOINTERFACE = unchecked((int)0x80004002); + + /// + /// The GUID for the IUnknown COM interface. + /// + public static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + + /// + /// The GUID for the IAgileObject WinRT interface. + /// + public static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); + + /// + /// The GUID for the IDispatcherQueueHandler WinRT interface. + /// + public static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); + } +} \ No newline at end of file From ed3dc5e5a51e7a78fca3a128014bda710b8e7c5d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 16:15:29 +0200 Subject: [PATCH 05/13] Added DispatcherQueueProxyHandler2 type --- .../DispatcherQueueExtensions{T}.cs | 118 +++++++++-- ...ler.cs => DispatcherQueueProxyHandler1.cs} | 41 ++-- .../Interop/DispatcherQueueProxyHandler2.cs | 195 ++++++++++++++++++ 3 files changed, 321 insertions(+), 33 deletions(-) rename CommunityToolkit.WinUI/Extensions/Interop/{DispatcherQueueProxyHandler.cs => DispatcherQueueProxyHandler1.cs} (87%) create mode 100644 CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index 993cb7bb81d..b4394c5334a 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -14,10 +14,21 @@ namespace CommunityToolkit.WinUI /// /// A callback that will be executed on the thread. /// - /// The type of state to receive as input. + /// The type of state to receive as input. /// The input state for the callback. - public delegate void DispatcherQueueHandler(TState state) - where TState : class; + public delegate void DispatcherQueueHandler(T state) + where T : class; + + /// + /// A callback that will be executed on the thread. + /// + /// The type of the first state to receive as input. + /// The type of the second state to receive as input. + /// The first input state for the callback. + /// The second input state for the callback. + public delegate void DispatcherQueueHandler(T1 state1, T2 state2) + where T1 : class + where T2 : class; /// /// Helpers for executing code in a . @@ -27,17 +38,17 @@ public static partial class DispatcherQueueExtensions /// /// Adds a task to the which will be executed on the thread associated with it. /// - /// The type of state to capture. + /// The type of state to capture. /// The target to invoke the code on. - /// The input callback to enqueue. + /// The input callback to enqueue. /// The input state to capture and pass to the callback. /// Whether or not the task was added to the queue. /// Thrown when the enqueue operation fails. - public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, TState state) - where TState : class + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T state) + where T : class { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); + DispatcherQueueProxyHandler1* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler1.Create(callback, state); bool success; int hResult; @@ -64,18 +75,99 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// /// Adds a task to the which will be executed on the thread associated with it. /// - /// The type of state to capture. + /// The type of state to capture. /// The target to invoke the code on. /// The desired priority for the callback to schedule. - /// The input callback to enqueue. + /// The input callback to enqueue. /// The input state to capture and pass to the callback. /// Whether or not the task was added to the queue. /// Thrown when the enqueue operation fails. - public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, TState state) - where TState : class + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T state) + where T : class + { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + DispatcherQueueProxyHandler1* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler1.Create(callback, state); + + bool success; + int hResult; + + try + { + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, (byte*)&success); + + GC.KeepAlive(dispatcherQueue); + } + finally + { + dispatcherQueueHandlerPtr->Release(); + } + + if (hResult != 0) + { + ExceptionHelpers.ThrowExceptionForHR(hResult); + } + + return success; + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The target to invoke the code on. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T1 state1, T2 state2) + where T1 : class + where T2 : class + { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + DispatcherQueueProxyHandler2* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler2.Create(callback, state1, state2); + + bool success; + int hResult; + + try + { + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, (byte*)&success); + + GC.KeepAlive(dispatcherQueue); + } + finally + { + dispatcherQueueHandlerPtr->Release(); + } + + if (hResult != 0) + { + ExceptionHelpers.ThrowExceptionForHR(hResult); + } + + return success; + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The target to invoke the code on. + /// The desired priority for the callback to schedule. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T1 state1, T2 state2) + where T1 : class + where T2 : class { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state); + DispatcherQueueProxyHandler2* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler2.Create(callback, state1, state2); bool success; int hResult; diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs similarity index 87% rename from CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs rename to CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 1cfacf21d06..952b88f4df4 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -18,17 +18,17 @@ namespace CommunityToolkit.WinUI.Interop /// A custom IDispatcherQueueHandler object, that internally stores a captured instance /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. /// - internal unsafe struct DispatcherQueueProxyHandler + internal unsafe struct DispatcherQueueProxyHandler1 { /// - /// The shared vtable pointer for instances. + /// The shared vtable pointer for instances. /// private static readonly void** Vtbl = InitVtbl(); /// - /// Setups the vtable pointer for . + /// Setups the vtable pointer for . /// - /// The initialized vtable pointer for . + /// The initialized vtable pointer for . /// /// The vtable itself is allocated with , /// which allocates memory in the high frequency heap associated with the input runtime type. This will be @@ -36,12 +36,12 @@ internal unsafe struct DispatcherQueueProxyHandler /// private static void** InitVtbl() { - void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler), sizeof(void*) * 4); + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler1), sizeof(void*) * 4); - lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; - lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; - lpVtbl[2] = (delegate* unmanaged)&Impl.Release; - lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; return lpVtbl; } @@ -67,16 +67,14 @@ internal unsafe struct DispatcherQueueProxyHandler private volatile uint referenceCount; /// - /// Creates a new instance for the input callback and state. + /// Creates a new instance for the input callback and state. /// - /// The type of state to capture. /// The input callback to enqueue. /// The input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler* Create(DispatcherQueueHandler handler, TState state) - where TState : class + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler1* Create(object handler, object state) { - DispatcherQueueProxyHandler* @this = (DispatcherQueueProxyHandler*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler)); + DispatcherQueueProxyHandler1* @this = (DispatcherQueueProxyHandler1*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler1)); @this->lpVtbl = Vtbl; @this->callbackHandle = GCHandle.Alloc(handler); @@ -99,6 +97,9 @@ public uint Release() if (referenceCount == 0) { + callbackHandle.Free(); + stateHandle.Free(); + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); } @@ -106,7 +107,7 @@ public uint Release() } /// - /// A private type with the implementation of the unmanaged methods for . + /// A private type with the implementation of the unmanaged methods for . /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. /// private static class Impl @@ -115,7 +116,7 @@ private static class Impl /// Implements IUnknown.QueryInterface(REFIID, void**). /// [UnmanagedCallersOnly] - public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, void** ppvObject) + public static int QueryInterface(DispatcherQueueProxyHandler1* @this, Guid* riid, void** ppvObject) { if (riid->Equals(IUnknown) || riid->Equals(IAgileObject) || @@ -135,7 +136,7 @@ public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, /// Implements IUnknown.AddRef(). /// [UnmanagedCallersOnly] - public static uint AddRef(DispatcherQueueProxyHandler* @this) + public static uint AddRef(DispatcherQueueProxyHandler1* @this) { return Interlocked.Increment(ref @this->referenceCount); } @@ -144,7 +145,7 @@ public static uint AddRef(DispatcherQueueProxyHandler* @this) /// Implements IUnknown.Release(). /// [UnmanagedCallersOnly] - public static uint Release(DispatcherQueueProxyHandler* @this) + public static uint Release(DispatcherQueueProxyHandler1* @this) { uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); @@ -163,7 +164,7 @@ public static uint Release(DispatcherQueueProxyHandler* @this) /// Implements IDispatcherQueueHandler.Invoke(). /// [UnmanagedCallersOnly] - public static int Invoke(DispatcherQueueProxyHandler* @this) + public static int Invoke(DispatcherQueueProxyHandler1* @this) { object callback = @this->callbackHandle.Target!; object state = @this->stateHandle.Target!; diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs new file mode 100644 index 00000000000..b714e85db74 --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -0,0 +1,195 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using static CommunityToolkit.WinUI.Interop.Windows; + +#nullable enable + +#pragma warning disable SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A custom IDispatcherQueueHandler object, that internally stores a captured instance + /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. + /// + internal unsafe struct DispatcherQueueProxyHandler2 + { + /// + /// The shared vtable pointer for instances. + /// + private static readonly void** Vtbl = InitVtbl(); + + /// + /// Setups the vtable pointer for . + /// + /// The initialized vtable pointer for . + /// + /// The vtable itself is allocated with , + /// which allocates memory in the high frequency heap associated with the input runtime type. This will be + /// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory. + /// + private static void** InitVtbl() + { + void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler2), sizeof(void*) * 4); + + lpVtbl[0] = (delegate* unmanaged)&Impl.QueryInterface; + lpVtbl[1] = (delegate* unmanaged)&Impl.AddRef; + lpVtbl[2] = (delegate* unmanaged)&Impl.Release; + lpVtbl[3] = (delegate* unmanaged)&Impl.Invoke; + + return lpVtbl; + } + + /// + /// The vtable pointer for the current instance. + /// + private void** lpVtbl; + + /// + /// The to the captured (for some unknown TState type). + /// + private GCHandle callbackHandle; + + /// + /// The to the first captured state (with an unknown T1 type). + /// + private GCHandle state1Handle; + + /// + /// The to the second captured state (with an unknown T2 type). + /// + private GCHandle state2Handle; + + /// + /// The current reference count for the object (from IUnknown). + /// + private volatile uint referenceCount; + + /// + /// Creates a new instance for the input callback and state. + /// + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler2* Create(object handler, object state1, object state2) + { + DispatcherQueueProxyHandler2* @this = (DispatcherQueueProxyHandler2*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler2)); + + @this->lpVtbl = Vtbl; + @this->callbackHandle = GCHandle.Alloc(handler); + @this->state1Handle = GCHandle.Alloc(state1); + @this->state2Handle = GCHandle.Alloc(state2); + @this->referenceCount = 1; + + return @this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint AddRef() + { + return Interlocked.Increment(ref referenceCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + uint referenceCount = Interlocked.Decrement(ref this.referenceCount); + + if (referenceCount == 0) + { + callbackHandle.Free(); + state1Handle.Free(); + state2Handle.Free(); + + Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); + } + + return referenceCount; + } + + /// + /// A private type with the implementation of the unmanaged methods for . + /// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. + /// + private static class Impl + { + /// + /// Implements IUnknown.QueryInterface(REFIID, void**). + /// + [UnmanagedCallersOnly] + public static int QueryInterface(DispatcherQueueProxyHandler2* @this, Guid* riid, void** ppvObject) + { + if (riid->Equals(IUnknown) || + riid->Equals(IAgileObject) || + riid->Equals(IDispatcherQueueHandler)) + { + @this->AddRef(); + + *ppvObject = @this; + + return S_OK; + } + + return E_NOINTERFACE; + } + + /// + /// Implements IUnknown.AddRef(). + /// + [UnmanagedCallersOnly] + public static uint AddRef(DispatcherQueueProxyHandler2* @this) + { + return Interlocked.Increment(ref @this->referenceCount); + } + + /// + /// Implements IUnknown.Release(). + /// + [UnmanagedCallersOnly] + public static uint Release(DispatcherQueueProxyHandler2* @this) + { + uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); + + if (referenceCount == 0) + { + @this->callbackHandle.Free(); + @this->state1Handle.Free(); + @this->state2Handle.Free(); + + Marshal.FreeHGlobal((IntPtr)@this); + } + + return referenceCount; + } + + /// + /// Implements IDispatcherQueueHandler.Invoke(). + /// + [UnmanagedCallersOnly] + public static int Invoke(DispatcherQueueProxyHandler2* @this) + { + object callback = @this->callbackHandle.Target!; + object state1 = @this->state1Handle.Target!; + object state2 = @this->state2Handle.Target!; + + try + { + // Same optimization as in DispatcherQueueProxyHandler1 + Unsafe.As>(callback)(state1, state2); + } + catch + { + } + + return S_OK; + } + } + } +} \ No newline at end of file From 0acbd237d6518c4e82e00118580f68b6f1e6d28b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jul 2021 16:29:35 +0200 Subject: [PATCH 06/13] More code refactoring --- .../DispatcherQueueExtensions{T}.cs | 99 ++++++++----------- .../Interop/DispatcherQueueProxyHandler1.cs | 14 +-- .../Interop/DispatcherQueueProxyHandler2.cs | 14 +-- .../Extensions/Interop/IDispatcherQueue.cs | 14 ++- .../Interop/IDispatcherQueueHandler.cs | 28 ++++++ .../Extensions/Interop/Windows.cs | 6 +- 6 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index b4394c5334a..a2733433993 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -47,29 +47,7 @@ public static partial class DispatcherQueueExtensions public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T state) where T : class { - IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler1* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler1.Create(callback, state); - - bool success; - int hResult; - - try - { - hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, (byte*)&success); - - GC.KeepAlive(dispatcherQueue); - } - finally - { - dispatcherQueueHandlerPtr->Release(); - } - - if (hResult != 0) - { - ExceptionHelpers.ThrowExceptionForHR(hResult); - } - - return success; + return TryEnqueue(dispatcherQueue, DispatcherQueueProxyHandler1.Create(callback, state)); } /// @@ -85,29 +63,7 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, Di public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T state) where T : class { - IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler1* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler1.Create(callback, state); - - bool success; - int hResult; - - try - { - hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, (byte*)&success); - - GC.KeepAlive(dispatcherQueue); - } - finally - { - dispatcherQueueHandlerPtr->Release(); - } - - if (hResult != 0) - { - ExceptionHelpers.ThrowExceptionForHR(hResult); - } - - return success; + return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler1.Create(callback, state)); } /// @@ -124,22 +80,52 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, Di public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T1 state1, T2 state2) where T1 : class where T2 : class + { + return TryEnqueue(dispatcherQueue, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The type of the first state to capture. + /// The type of the second state to capture. + /// The target to invoke the code on. + /// The desired priority for the callback to schedule. + /// The input callback to enqueue. + /// The first input state to capture and pass to the callback. + /// The second input state to capture and pass to the callback. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T1 state1, T2 state2) + where T1 : class + where T2 : class + { + return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); + } + + /// + /// Adds a task to the which will be executed on the thread associated with it. + /// + /// The target to invoke the code on. + /// The input callback to enqueue. + /// Whether or not the task was added to the queue. + /// Thrown when the enqueue operation fails. + private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, IDispatcherQueueHandler* dispatcherQueueHandler) { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler2* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler2.Create(callback, state1, state2); bool success; int hResult; try { - hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, (byte*)&success); + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); GC.KeepAlive(dispatcherQueue); } finally { - dispatcherQueueHandlerPtr->Release(); + dispatcherQueueHandler->Release(); } if (hResult != 0) @@ -153,34 +139,27 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// /// Adds a task to the which will be executed on the thread associated with it. /// - /// The type of the first state to capture. - /// The type of the second state to capture. /// The target to invoke the code on. /// The desired priority for the callback to schedule. - /// The input callback to enqueue. - /// The first input state to capture and pass to the callback. - /// The second input state to capture and pass to the callback. + /// The input callback to enqueue. /// Whether or not the task was added to the queue. /// Thrown when the enqueue operation fails. - public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler callback, T1 state1, T2 state2) - where T1 : class - where T2 : class + private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, IDispatcherQueueHandler* dispatcherQueueHandler) { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - DispatcherQueueProxyHandler2* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler2.Create(callback, state1, state2); bool success; int hResult; try { - hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, (byte*)&success); + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandler, (byte*)&success); GC.KeepAlive(dispatcherQueue); } finally { - dispatcherQueueHandlerPtr->Release(); + dispatcherQueueHandler->Release(); } if (hResult != 0) diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 952b88f4df4..76ce707828e 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -67,12 +67,12 @@ internal unsafe struct DispatcherQueueProxyHandler1 private volatile uint referenceCount; /// - /// Creates a new instance for the input callback and state. + /// Creates a new instance for the input callback and state. /// /// The input callback to enqueue. /// The input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler1* Create(object handler, object state) + /// A pointer to the newly initialized instance. + public static IDispatcherQueueHandler* Create(object handler, object state) { DispatcherQueueProxyHandler1* @this = (DispatcherQueueProxyHandler1*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler1)); @@ -81,7 +81,7 @@ internal unsafe struct DispatcherQueueProxyHandler1 @this->stateHandle = GCHandle.Alloc(state); @this->referenceCount = 1; - return @this; + return (IDispatcherQueueHandler*)@this; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -118,9 +118,9 @@ private static class Impl [UnmanagedCallersOnly] public static int QueryInterface(DispatcherQueueProxyHandler1* @this, Guid* riid, void** ppvObject) { - if (riid->Equals(IUnknown) || - riid->Equals(IAgileObject) || - riid->Equals(IDispatcherQueueHandler)) + if (riid->Equals(GuidOfIUnknown) || + riid->Equals(GuidOfIAgileObject) || + riid->Equals(GuidOfIDispatcherQueueHandler)) { @this->AddRef(); diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs index b714e85db74..25f70d76419 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -72,13 +72,13 @@ internal unsafe struct DispatcherQueueProxyHandler2 private volatile uint referenceCount; /// - /// Creates a new instance for the input callback and state. + /// Creates a new instance for the input callback and state. /// /// The input callback to enqueue. /// The first input state to capture and pass to the callback. /// The second input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler2* Create(object handler, object state1, object state2) + /// A pointer to the newly initialized instance. + public static IDispatcherQueueHandler* Create(object handler, object state1, object state2) { DispatcherQueueProxyHandler2* @this = (DispatcherQueueProxyHandler2*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler2)); @@ -88,7 +88,7 @@ internal unsafe struct DispatcherQueueProxyHandler2 @this->state2Handle = GCHandle.Alloc(state2); @this->referenceCount = 1; - return @this; + return (IDispatcherQueueHandler*)@this; } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -126,9 +126,9 @@ private static class Impl [UnmanagedCallersOnly] public static int QueryInterface(DispatcherQueueProxyHandler2* @this, Guid* riid, void** ppvObject) { - if (riid->Equals(IUnknown) || - riid->Equals(IAgileObject) || - riid->Equals(IDispatcherQueueHandler)) + if (riid->Equals(GuidOfIUnknown) || + riid->Equals(GuidOfIAgileObject) || + riid->Equals(GuidOfIDispatcherQueueHandler)) { @this->AddRef(); diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs index b95b9bcac38..4dbad8b5322 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -5,8 +5,6 @@ using System.Runtime.CompilerServices; using Microsoft.UI.Dispatching; -#nullable enable - #pragma warning disable CS0649, SA1023 namespace CommunityToolkit.WinUI.Interop @@ -21,26 +19,26 @@ internal unsafe struct IDispatcherQueue /// /// Native API for . /// - /// A pointer to an IDispatcherQueueHandler object. + /// A pointer to an object. /// The result of the operation (the WinRT retval). /// The HRESULT for the operation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueue(void* callback, byte* result) + public int TryEnqueue(IDispatcherQueueHandler* callback, byte* result) { - return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); + return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); } /// /// Native API for . /// /// The priority for the input callback. - /// A pointer to an IDispatcherQueueHandler object. + /// A pointer to an object. /// The result of the operation (the WinRT retval). /// The HRESULT for the operation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) + public int TryEnqueueWithPriority(DispatcherQueuePriority priority, IDispatcherQueueHandler* callback, byte* result) { - return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); + return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); } } } \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs new file mode 100644 index 00000000000..587c2f7e32c --- /dev/null +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs @@ -0,0 +1,28 @@ +// 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.Runtime.CompilerServices; + +#pragma warning disable CS0649, SA1023 + +namespace CommunityToolkit.WinUI.Interop +{ + /// + /// A struct mapping the native WinRT IDispatcherQueueHandler interface. + /// + internal unsafe struct IDispatcherQueueHandler + { + private readonly void** lpVtbl; + + /// + /// Native API for IUnknown.Release(). + /// + /// The updated reference count for the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public uint Release() + { + return ((delegate* unmanaged)lpVtbl[2])((IDispatcherQueueHandler*)Unsafe.AsPointer(ref this)); + } + } +} \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs index 0ded78bd108..95df0bf79e9 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/Windows.cs @@ -24,16 +24,16 @@ internal static class Windows /// /// The GUID for the IUnknown COM interface. /// - public static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); + public static readonly Guid GuidOfIUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); /// /// The GUID for the IAgileObject WinRT interface. /// - public static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); + public static readonly Guid GuidOfIAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90); /// /// The GUID for the IDispatcherQueueHandler WinRT interface. /// - public static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); + public static readonly Guid GuidOfIDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8); } } \ No newline at end of file From e5270d3baab9b50dc3753fbd0b40f96933168b89 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jul 2021 01:25:41 +0200 Subject: [PATCH 07/13] Enabled exception propagation from WinRT callbacks --- .../Extensions/DispatcherQueueExtensions{T}.cs | 8 ++++---- .../Extensions/Interop/DispatcherQueueProxyHandler1.cs | 6 +++++- .../Extensions/Interop/DispatcherQueueProxyHandler2.cs | 6 +++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index a2733433993..31b26445df9 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -112,13 +112,13 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// Thrown when the enqueue operation fails. private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, IDispatcherQueueHandler* dispatcherQueueHandler) { - IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - bool success; int hResult; try { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); GC.KeepAlive(dispatcherQueue); @@ -146,13 +146,13 @@ private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, IDispatch /// Thrown when the enqueue operation fails. private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, IDispatcherQueueHandler* dispatcherQueueHandler) { - IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - bool success; int hResult; try { + IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandler, (byte*)&success); GC.KeepAlive(dispatcherQueue); diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 76ce707828e..42e4bf5dd50 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using WinRT; using static CommunityToolkit.WinUI.Interop.Windows; #nullable enable @@ -179,8 +180,11 @@ public static int Invoke(DispatcherQueueProxyHandler1* @this) // need to make the proxy type itself generic, so without knowing the actual type argument. Unsafe.As>(callback)(state); } - catch + catch (Exception e) { + ExceptionHelpers.SetErrorInfo(e); + + return ExceptionHelpers.GetHRForException(e); } return S_OK; diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs index 25f70d76419..936b6ee11ac 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using WinRT; using static CommunityToolkit.WinUI.Interop.Windows; #nullable enable @@ -184,8 +185,11 @@ public static int Invoke(DispatcherQueueProxyHandler2* @this) // Same optimization as in DispatcherQueueProxyHandler1 Unsafe.As>(callback)(state1, state2); } - catch + catch (Exception e) { + ExceptionHelpers.SetErrorInfo(e); + + return ExceptionHelpers.GetHRForException(e); } return S_OK; From e4c8ae65eff3aa39dd20002f68e153d972e2deba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jul 2021 15:17:02 +0200 Subject: [PATCH 08/13] Minor code refactoring --- .../DispatcherQueueExtensions{T}.cs | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index 31b26445df9..0fde3bb683c 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -47,7 +47,7 @@ public static partial class DispatcherQueueExtensions public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler callback, T state) where T : class { - return TryEnqueue(dispatcherQueue, DispatcherQueueProxyHandler1.Create(callback, state)); + return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler1.Create(callback, state)); } /// @@ -81,7 +81,7 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu where T1 : class where T2 : class { - return TryEnqueue(dispatcherQueue, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); + return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); } /// @@ -107,10 +107,11 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// Adds a task to the which will be executed on the thread associated with it. /// /// The target to invoke the code on. + /// The desired priority for the callback to schedule (if available). /// The input callback to enqueue. /// Whether or not the task was added to the queue. /// Thrown when the enqueue operation fails. - private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, IDispatcherQueueHandler* dispatcherQueueHandler) + private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority? priority, IDispatcherQueueHandler* dispatcherQueueHandler) { bool success; int hResult; @@ -119,41 +120,14 @@ private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, IDispatch { IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); - - GC.KeepAlive(dispatcherQueue); - } - finally - { - dispatcherQueueHandler->Release(); - } - - if (hResult != 0) - { - ExceptionHelpers.ThrowExceptionForHR(hResult); - } - - return success; - } - - /// - /// Adds a task to the which will be executed on the thread associated with it. - /// - /// The target to invoke the code on. - /// The desired priority for the callback to schedule. - /// The input callback to enqueue. - /// Whether or not the task was added to the queue. - /// Thrown when the enqueue operation fails. - private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, IDispatcherQueueHandler* dispatcherQueueHandler) - { - bool success; - int hResult; - - try - { - IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; - - hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandler, (byte*)&success); + if (priority.HasValue) + { + hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority.GetValueOrDefault(), dispatcherQueueHandler, (byte*)&success); + } + else + { + hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); + } GC.KeepAlive(dispatcherQueue); } From 361fc3f08c9a0383adf954e1e1973459847e4a2c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jul 2021 15:32:02 +0200 Subject: [PATCH 09/13] Minor XML docs improvements --- .../Extensions/Interop/DispatcherQueueProxyHandler1.cs | 8 ++++++++ .../Extensions/Interop/DispatcherQueueProxyHandler2.cs | 8 ++++++++ .../Extensions/Interop/IDispatcherQueue.cs | 3 +++ .../Extensions/Interop/IDispatcherQueueHandler.cs | 3 +++ 4 files changed, 22 insertions(+) diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 42e4bf5dd50..17b580d5687 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -85,12 +85,20 @@ internal unsafe struct DispatcherQueueProxyHandler1 return (IDispatcherQueueHandler*)@this; } + /// + /// Devirtualized API for IUnknown.AddRef(). + /// + /// The updated reference count for the current instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint AddRef() { return Interlocked.Increment(ref referenceCount); } + /// + /// Devirtualized API for IUnknown.Release(). + /// + /// The updated reference count for the current instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint Release() { diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs index 936b6ee11ac..b82d220b1e2 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -92,12 +92,20 @@ internal unsafe struct DispatcherQueueProxyHandler2 return (IDispatcherQueueHandler*)@this; } + /// + /// Devirtualized API for IUnknown.AddRef(). + /// + /// The updated reference count for the current instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint AddRef() { return Interlocked.Increment(ref referenceCount); } + /// + /// Devirtualized API for IUnknown.Release(). + /// + /// The updated reference count for the current instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint Release() { diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs index 4dbad8b5322..0530c2d6d85 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -14,6 +14,9 @@ namespace CommunityToolkit.WinUI.Interop /// internal unsafe struct IDispatcherQueue { + /// + /// The vtable pointer for the current instance. + /// private readonly void** lpVtbl; /// diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs index 587c2f7e32c..f6d6dde6039 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs @@ -13,6 +13,9 @@ namespace CommunityToolkit.WinUI.Interop /// internal unsafe struct IDispatcherQueueHandler { + /// + /// The vtable pointer for the current instance. + /// private readonly void** lpVtbl; /// From 42d227f3f34d4b218e645d3a8ab9428f814132fd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jul 2021 12:55:21 +0200 Subject: [PATCH 10/13] Enabled inlining for calls to IDispatcherQueueHandler.Release() --- .../DispatcherQueueExtensions{T}.cs | 3 ++- .../Interop/DispatcherQueueProxyHandler1.cs | 10 ++++----- .../Interop/DispatcherQueueProxyHandler2.cs | 10 ++++----- .../Extensions/Interop/IDispatcherQueue.cs | 14 +++++++++---- .../Interop/IDispatcherQueueHandler.cs | 21 ++++--------------- 5 files changed, 26 insertions(+), 32 deletions(-) diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index 0fde3bb683c..d7eec1c71f0 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -111,7 +111,8 @@ public static unsafe bool TryEnqueue(this DispatcherQueue dispatcherQueu /// The input callback to enqueue. /// Whether or not the task was added to the queue. /// Thrown when the enqueue operation fails. - private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority? priority, IDispatcherQueueHandler* dispatcherQueueHandler) + private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, DispatcherQueuePriority? priority, THandler* dispatcherQueueHandler) + where THandler : unmanaged, IDispatcherQueueHandler { bool success; int hResult; diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 17b580d5687..4f1f8fc4a44 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.WinUI.Interop /// A custom IDispatcherQueueHandler object, that internally stores a captured instance /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. /// - internal unsafe struct DispatcherQueueProxyHandler1 + internal unsafe struct DispatcherQueueProxyHandler1 : IDispatcherQueueHandler { /// /// The shared vtable pointer for instances. @@ -68,12 +68,12 @@ internal unsafe struct DispatcherQueueProxyHandler1 private volatile uint referenceCount; /// - /// Creates a new instance for the input callback and state. + /// Creates a new instance for the input callback and state. /// /// The input callback to enqueue. /// The input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static IDispatcherQueueHandler* Create(object handler, object state) + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler1* Create(object handler, object state) { DispatcherQueueProxyHandler1* @this = (DispatcherQueueProxyHandler1*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler1)); @@ -82,7 +82,7 @@ internal unsafe struct DispatcherQueueProxyHandler1 @this->stateHandle = GCHandle.Alloc(state); @this->referenceCount = 1; - return (IDispatcherQueueHandler*)@this; + return @this; } /// diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs index b82d220b1e2..48868a52331 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.WinUI.Interop /// A custom IDispatcherQueueHandler object, that internally stores a captured instance /// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. /// - internal unsafe struct DispatcherQueueProxyHandler2 + internal unsafe struct DispatcherQueueProxyHandler2 : IDispatcherQueueHandler { /// /// The shared vtable pointer for instances. @@ -73,13 +73,13 @@ internal unsafe struct DispatcherQueueProxyHandler2 private volatile uint referenceCount; /// - /// Creates a new instance for the input callback and state. + /// Creates a new instance for the input callback and state. /// /// The input callback to enqueue. /// The first input state to capture and pass to the callback. /// The second input state to capture and pass to the callback. - /// A pointer to the newly initialized instance. - public static IDispatcherQueueHandler* Create(object handler, object state1, object state2) + /// A pointer to the newly initialized instance. + public static DispatcherQueueProxyHandler2* Create(object handler, object state1, object state2) { DispatcherQueueProxyHandler2* @this = (DispatcherQueueProxyHandler2*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler2)); @@ -89,7 +89,7 @@ internal unsafe struct DispatcherQueueProxyHandler2 @this->state2Handle = GCHandle.Alloc(state2); @this->referenceCount = 1; - return (IDispatcherQueueHandler*)@this; + return @this; } /// diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs index 0530c2d6d85..c9950307764 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueue.cs @@ -25,10 +25,16 @@ internal unsafe struct IDispatcherQueue /// A pointer to an object. /// The result of the operation (the WinRT retval). /// The HRESULT for the operation. + /// + /// The parameter is assumed to be a pointer to an object, but it + /// is just typed as a to avoid unnecessary generic instantiations for this method (as it just needs to pass a + /// pointer to the native side anyway). The is an actual C# interface and not a C++ one as it + /// is only used internally to constrain type parameters and allow calls to to be inlined. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueue(IDispatcherQueueHandler* callback, byte* result) + public int TryEnqueue(void* callback, byte* result) { - return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); + return ((delegate* unmanaged)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result); } /// @@ -39,9 +45,9 @@ public int TryEnqueue(IDispatcherQueueHandler* callback, byte* result) /// The result of the operation (the WinRT retval). /// The HRESULT for the operation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int TryEnqueueWithPriority(DispatcherQueuePriority priority, IDispatcherQueueHandler* callback, byte* result) + public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result) { - return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); + return ((delegate* unmanaged)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result); } } } \ No newline at end of file diff --git a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs index f6d6dde6039..af01bb29c63 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/IDispatcherQueueHandler.cs @@ -2,30 +2,17 @@ // 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.Runtime.CompilerServices; - -#pragma warning disable CS0649, SA1023 - namespace CommunityToolkit.WinUI.Interop { /// - /// A struct mapping the native WinRT IDispatcherQueueHandler interface. + /// An interface mapping the native WinRT IDispatcherQueueHandler interface. /// - internal unsafe struct IDispatcherQueueHandler + internal interface IDispatcherQueueHandler { /// - /// The vtable pointer for the current instance. - /// - private readonly void** lpVtbl; - - /// - /// Native API for IUnknown.Release(). + /// Implements IUnknown.Release(). /// /// The updated reference count for the current instance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public uint Release() - { - return ((delegate* unmanaged)lpVtbl[2])((IDispatcherQueueHandler*)Unsafe.AsPointer(ref this)); - } + uint Release(); } } \ No newline at end of file From f17294689509323df0011d23f06d808941abfb4b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 6 Jul 2021 19:34:41 +0200 Subject: [PATCH 11/13] Improved docs for IDispatcherQueueHandler.Release() call --- .../Extensions/DispatcherQueueExtensions{T}.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs index d7eec1c71f0..080f1f15c29 100644 --- a/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs +++ b/CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs @@ -134,6 +134,11 @@ private static unsafe bool TryEnqueue(DispatcherQueue dispatcherQueue, } finally { + // This call doesn not have a corresponding AddRef() invocation that is visible, and + // that is because the static constructors for all existing custom handlers already + // set the internal reference count to 1. Structuring the code like this makes it + // possible to centralize all logic in this method without each caller needing to + // explicitly have a try/finally block for the allocated handler that is passed here. dispatcherQueueHandler->Release(); } From 89a9aa4486a1ddb0ebe88cea749bbda980f9a5bc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Aug 2021 14:10:27 +0200 Subject: [PATCH 12/13] Added unit tests for new DispatcherQueue extensions --- .../Test_DispatcherQueueExtensions.cs | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs b/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs index 5313f5bdcd8..6d1663cf68e 100644 --- a/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs +++ b/UnitTests/UnitTests.UWP/Extensions/Test_DispatcherQueueExtensions.cs @@ -287,5 +287,167 @@ public void Test_DispatcherQueueHelper_FuncOfTaskOfT_Exception() Assert.IsNotNull(task.Exception); Assert.IsInstanceOfType(task.Exception.InnerExceptions.FirstOrDefault(), typeof(ArgumentException)); } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue(x => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread)); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + taskSource.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1_Ok_NonUIThread)); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue((x, tcs) => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread)); + Assert.AreSame(tcs, taskSource); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + tcs.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_TryEnqueueT1T2_Ok_NonUIThread), taskSource); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue( + DispatcherQueuePriority.Normal, + x => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread)); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + taskSource.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1_Ok_NonUIThread)); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } + + [TestCategory("DispatcherQueueExtensions")] + [TestMethod] + public async Task Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread() + { + TaskCompletionSource taskSource = new(); + + _ = CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync( + CoreDispatcherPriority.Normal, + async () => + { + try + { + var dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + bool success = dispatcherQueue.TryEnqueue( + DispatcherQueuePriority.Normal, + (x, tcs) => + { + Assert.AreEqual(x, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread)); + Assert.AreSame(tcs, taskSource); + + var textBlock = new TextBlock { Text = x }; + + Assert.AreEqual(textBlock.Text, x); + + tcs.SetResult(); + }, nameof(Test_DispatcherQueueExtensions_Stateful_WithPriority_TryEnqueueT1T2_Ok_NonUIThread), taskSource); + + if (!success) + { + taskSource.SetException(new Exception("Failed to enqueue task")); + } + } + catch (Exception e) + { + taskSource.SetException(e); + } + }); + + return taskSource.Task; + } } } \ No newline at end of file From 5c6534d010aa0069b07fb301f8af391f82e487d9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 12 Sep 2021 19:08:09 +0200 Subject: [PATCH 13/13] Fixed undefined behavior with delegate reinterpret cast --- .../Interop/DispatcherQueueProxyHandler1.cs | 27 +++++++++++++------ .../Interop/DispatcherQueueProxyHandler2.cs | 27 ++++++++++++++++--- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs index 4f1f8fc4a44..f7dc8efd514 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs @@ -62,6 +62,11 @@ internal unsafe struct DispatcherQueueProxyHandler1 : IDispatcherQueueHandler /// private GCHandle stateHandle; + /// + /// The generic stub to invoke the current callback with the right generic context. + /// + private delegate* stub; + /// /// The current reference count for the object (from IUnknown). /// @@ -70,16 +75,19 @@ internal unsafe struct DispatcherQueueProxyHandler1 : IDispatcherQueueHandler /// /// Creates a new instance for the input callback and state. /// + /// The type of state currently being used. /// The input callback to enqueue. /// The input state to capture and pass to the callback. /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler1* Create(object handler, object state) + public static DispatcherQueueProxyHandler1* Create(DispatcherQueueHandler handler, T state) + where T : class { DispatcherQueueProxyHandler1* @this = (DispatcherQueueProxyHandler1*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler1)); @this->lpVtbl = Vtbl; @this->callbackHandle = GCHandle.Alloc(handler); @this->stateHandle = GCHandle.Alloc(state); + @this->stub = &Impl.Invoke; @this->referenceCount = 1; return @this; @@ -174,19 +182,22 @@ public static uint Release(DispatcherQueueProxyHandler1* @this) /// [UnmanagedCallersOnly] public static int Invoke(DispatcherQueueProxyHandler1* @this) + { + return @this->stub(@this); + } + + /// + /// Implements IDispatcherQueueHandler.Invoke() from within a generic context. + /// + public static int Invoke(DispatcherQueueProxyHandler1* @this) + where T : class { object callback = @this->callbackHandle.Target!; object state = @this->stateHandle.Target!; try { - // We do an unsafe cast here to treat the captured delegate as if the contravariant - // input type was actually declared as covariant. This is valid because the type - // parameter is constrained to be a reference type, and due to how the proxy handler - // is constructed we know that the captured state will always match the actual type - // of the captured handler at this point. This lets this whole method work without the - // need to make the proxy type itself generic, so without knowing the actual type argument. - Unsafe.As>(callback)(state); + Unsafe.As>(callback)(Unsafe.As(state)); } catch (Exception e) { diff --git a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs index 48868a52331..cfb4161313d 100644 --- a/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs +++ b/CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler2.cs @@ -67,6 +67,11 @@ internal unsafe struct DispatcherQueueProxyHandler2 : IDispatcherQueueHandler /// private GCHandle state2Handle; + /// + /// The generic stub to invoke the current callback with the right generic context. + /// + private delegate* stub; + /// /// The current reference count for the object (from IUnknown). /// @@ -75,11 +80,15 @@ internal unsafe struct DispatcherQueueProxyHandler2 : IDispatcherQueueHandler /// /// Creates a new instance for the input callback and state. /// + /// The type of the first state to capture. + /// The type of the second state to capture. /// The input callback to enqueue. /// The first input state to capture and pass to the callback. /// The second input state to capture and pass to the callback. /// A pointer to the newly initialized instance. - public static DispatcherQueueProxyHandler2* Create(object handler, object state1, object state2) + public static DispatcherQueueProxyHandler2* Create(DispatcherQueueHandler handler, T1 state1, T2 state2) + where T1 : class + where T2 : class { DispatcherQueueProxyHandler2* @this = (DispatcherQueueProxyHandler2*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler2)); @@ -87,6 +96,7 @@ internal unsafe struct DispatcherQueueProxyHandler2 : IDispatcherQueueHandler @this->callbackHandle = GCHandle.Alloc(handler); @this->state1Handle = GCHandle.Alloc(state1); @this->state2Handle = GCHandle.Alloc(state2); + @this->stub = &Impl.Invoke; @this->referenceCount = 1; return @this; @@ -183,6 +193,16 @@ public static uint Release(DispatcherQueueProxyHandler2* @this) /// [UnmanagedCallersOnly] public static int Invoke(DispatcherQueueProxyHandler2* @this) + { + return @this->stub(@this); + } + + /// + /// Implements IDispatcherQueueHandler.Invoke() from within a generic context. + /// + public static int Invoke(DispatcherQueueProxyHandler2* @this) + where T1 : class + where T2 : class { object callback = @this->callbackHandle.Target!; object state1 = @this->state1Handle.Target!; @@ -190,8 +210,9 @@ public static int Invoke(DispatcherQueueProxyHandler2* @this) try { - // Same optimization as in DispatcherQueueProxyHandler1 - Unsafe.As>(callback)(state1, state2); + Unsafe.As>(callback)( + Unsafe.As(state1), + Unsafe.As(state2)); } catch (Exception e) {