-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Feature/stateful dispatcherqueue extensions #4097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Sergio0694
wants to merge
13
commits into
CommunityToolkit:winui
Choose a base branch
from
Sergio0694:feature/stateful-dispatcherqueue-extensions
base: winui
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4342454
Added DispatcherQueue.TryEnqueue<TState> extensions
Sergio0694 6870630
Added exception handling for WinRT calls
Sergio0694 332f96e
Minor code refactoring
Sergio0694 39fba58
More code refactoring
Sergio0694 ed3dc5e
Added DispatcherQueueProxyHandler2 type
Sergio0694 0acbd23
More code refactoring
Sergio0694 e5270d3
Enabled exception propagation from WinRT callbacks
Sergio0694 e4c8ae6
Minor code refactoring
Sergio0694 361fc3f
Minor XML docs improvements
Sergio0694 42d227f
Enabled inlining for calls to IDispatcherQueueHandler.Release()
Sergio0694 f172946
Improved docs for IDispatcherQueueHandler.Release() call
Sergio0694 89a9aa4
Added unit tests for new DispatcherQueue extensions
Sergio0694 5c6534d
Fixed undefined behavior with delegate reinterpret cast
Sergio0694 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
153 changes: 153 additions & 0 deletions
153
CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
// 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 CommunityToolkit.WinUI.Interop; | ||
using Microsoft.UI.Dispatching; | ||
using WinRT; | ||
|
||
#nullable enable | ||
|
||
namespace CommunityToolkit.WinUI | ||
{ | ||
/// <summary> | ||
/// A callback that will be executed on the <see cref="DispatcherQueue"/> thread. | ||
/// </summary> | ||
/// <typeparam name="T">The type of state to receive as input.</typeparam> | ||
/// <param name="state">The input state for the callback.</param> | ||
public delegate void DispatcherQueueHandler<in T>(T state) | ||
where T : class; | ||
|
||
/// <summary> | ||
/// A callback that will be executed on the <see cref="DispatcherQueue"/> thread. | ||
/// </summary> | ||
/// <typeparam name="T1">The type of the first state to receive as input.</typeparam> | ||
/// <typeparam name="T2">The type of the second state to receive as input.</typeparam> | ||
/// <param name="state1">The first input state for the callback.</param> | ||
/// <param name="state2">The second input state for the callback.</param> | ||
public delegate void DispatcherQueueHandler<in T1, in T2>(T1 state1, T2 state2) | ||
where T1 : class | ||
where T2 : class; | ||
|
||
/// <summary> | ||
/// Helpers for executing code in a <see cref="DispatcherQueue"/>. | ||
/// </summary> | ||
public static partial class DispatcherQueueExtensions | ||
{ | ||
/// <summary> | ||
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it. | ||
/// </summary> | ||
/// <typeparam name="T">The type of state to capture.</typeparam> | ||
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param> | ||
/// <param name="callback">The input <see cref="DispatcherQueueHandler{T}"/> callback to enqueue.</param> | ||
/// <param name="state">The input state to capture and pass to the callback.</param> | ||
/// <returns>Whether or not the task was added to the queue.</returns> | ||
/// <exception cref="Exception">Thrown when the enqueue operation fails.</exception> | ||
public static unsafe bool TryEnqueue<T>(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler<T> callback, T state) | ||
where T : class | ||
{ | ||
return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler1.Create(callback, state)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it. | ||
/// </summary> | ||
/// <typeparam name="T">The type of state to capture.</typeparam> | ||
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param> | ||
/// <param name="priority"> The desired priority for the callback to schedule.</param> | ||
/// <param name="callback">The input <see cref="DispatcherQueueHandler{T}"/> callback to enqueue.</param> | ||
/// <param name="state">The input state to capture and pass to the callback.</param> | ||
/// <returns>Whether or not the task was added to the queue.</returns> | ||
/// <exception cref="Exception">Thrown when the enqueue operation fails.</exception> | ||
public static unsafe bool TryEnqueue<T>(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler<T> callback, T state) | ||
where T : class | ||
{ | ||
return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler1.Create(callback, state)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it. | ||
/// </summary> | ||
/// <typeparam name="T1">The type of the first state to capture.</typeparam> | ||
/// <typeparam name="T2">The type of the second state to capture.</typeparam> | ||
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param> | ||
/// <param name="callback">The input <see cref="DispatcherQueueHandler{T}"/> callback to enqueue.</param> | ||
/// <param name="state1">The first input state to capture and pass to the callback.</param> | ||
/// <param name="state2">The second input state to capture and pass to the callback.</param> | ||
/// <returns>Whether or not the task was added to the queue.</returns> | ||
/// <exception cref="Exception">Thrown when the enqueue operation fails.</exception> | ||
public static unsafe bool TryEnqueue<T1, T2>(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler<T1, T2> callback, T1 state1, T2 state2) | ||
where T1 : class | ||
where T2 : class | ||
{ | ||
return TryEnqueue(dispatcherQueue, null, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it. | ||
/// </summary> | ||
/// <typeparam name="T1">The type of the first state to capture.</typeparam> | ||
/// <typeparam name="T2">The type of the second state to capture.</typeparam> | ||
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param> | ||
/// <param name="priority"> The desired priority for the callback to schedule.</param> | ||
/// <param name="callback">The input <see cref="DispatcherQueueHandler{T}"/> callback to enqueue.</param> | ||
/// <param name="state1">The first input state to capture and pass to the callback.</param> | ||
/// <param name="state2">The second input state to capture and pass to the callback.</param> | ||
/// <returns>Whether or not the task was added to the queue.</returns> | ||
/// <exception cref="Exception">Thrown when the enqueue operation fails.</exception> | ||
public static unsafe bool TryEnqueue<T1, T2>(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler<T1, T2> callback, T1 state1, T2 state2) | ||
where T1 : class | ||
where T2 : class | ||
{ | ||
return TryEnqueue(dispatcherQueue, priority, DispatcherQueueProxyHandler2.Create(callback, state1, state2)); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it. | ||
/// </summary> | ||
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param> | ||
/// <param name="priority"> The desired priority for the callback to schedule (if available).</param> | ||
/// <param name="dispatcherQueueHandler">The input callback to enqueue.</param> | ||
/// <returns>Whether or not the task was added to the queue.</returns> | ||
/// <exception cref="Exception">Thrown when the enqueue operation fails.</exception> | ||
private static unsafe bool TryEnqueue<THandler>(DispatcherQueue dispatcherQueue, DispatcherQueuePriority? priority, THandler* dispatcherQueueHandler) | ||
where THandler : unmanaged, IDispatcherQueueHandler | ||
{ | ||
bool success; | ||
int hResult; | ||
|
||
try | ||
{ | ||
IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr; | ||
|
||
if (priority.HasValue) | ||
{ | ||
hResult = dispatcherQueuePtr->TryEnqueueWithPriority(priority.GetValueOrDefault(), dispatcherQueueHandler, (byte*)&success); | ||
} | ||
else | ||
{ | ||
hResult = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandler, (byte*)&success); | ||
} | ||
|
||
GC.KeepAlive(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(); | ||
} | ||
|
||
if (hResult != 0) | ||
{ | ||
ExceptionHelpers.ThrowExceptionForHR(hResult); | ||
} | ||
|
||
return success; | ||
} | ||
} | ||
} |
213 changes: 213 additions & 0 deletions
213
CommunityToolkit.WinUI/Extensions/Interop/DispatcherQueueProxyHandler1.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
// 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 WinRT; | ||
using static CommunityToolkit.WinUI.Interop.Windows; | ||
|
||
#nullable enable | ||
|
||
#pragma warning disable SA1023 | ||
|
||
namespace CommunityToolkit.WinUI.Interop | ||
{ | ||
/// <summary> | ||
/// A custom <c>IDispatcherQueueHandler</c> object, that internally stores a captured <see cref="DispatcherQueueHandler{TState}"/> instance | ||
/// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations. | ||
/// </summary> | ||
internal unsafe struct DispatcherQueueProxyHandler1 : IDispatcherQueueHandler | ||
{ | ||
/// <summary> | ||
/// The shared vtable pointer for <see cref="DispatcherQueueProxyHandler1"/> instances. | ||
/// </summary> | ||
private static readonly void** Vtbl = InitVtbl(); | ||
|
||
/// <summary> | ||
/// Setups the vtable pointer for <see cref="DispatcherQueueProxyHandler1"/>. | ||
/// </summary> | ||
/// <returns>The initialized vtable pointer for <see cref="DispatcherQueueProxyHandler1"/>.</returns> | ||
/// <remarks> | ||
/// The vtable itself is allocated with <see cref="RuntimeHelpers.AllocateTypeAssociatedMemory(Type, int)"/>, | ||
/// 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. | ||
/// </remarks> | ||
private static void** InitVtbl() | ||
{ | ||
void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler1), sizeof(void*) * 4); | ||
|
||
lpVtbl[0] = (delegate* unmanaged<DispatcherQueueProxyHandler1*, Guid*, void**, int>)&Impl.QueryInterface; | ||
lpVtbl[1] = (delegate* unmanaged<DispatcherQueueProxyHandler1*, uint>)&Impl.AddRef; | ||
lpVtbl[2] = (delegate* unmanaged<DispatcherQueueProxyHandler1*, uint>)&Impl.Release; | ||
lpVtbl[3] = (delegate* unmanaged<DispatcherQueueProxyHandler1*, int>)&Impl.Invoke; | ||
|
||
return lpVtbl; | ||
} | ||
|
||
/// <summary> | ||
/// The vtable pointer for the current instance. | ||
/// </summary> | ||
private void** lpVtbl; | ||
|
||
/// <summary> | ||
/// The <see cref="GCHandle"/> to the captured <see cref="DispatcherQueueHandler{TState}"/> (for some unknown <c>TState</c> type). | ||
/// </summary> | ||
private GCHandle callbackHandle; | ||
|
||
/// <summary> | ||
/// The <see cref="GCHandle"/> to the captured state (with an unknown <c>TState</c> type). | ||
/// </summary> | ||
private GCHandle stateHandle; | ||
|
||
/// <summary> | ||
/// The generic stub to invoke the current callback with the right generic context. | ||
/// </summary> | ||
private delegate*<DispatcherQueueProxyHandler1*, int> stub; | ||
|
||
/// <summary> | ||
/// The current reference count for the object (from <c>IUnknown</c>). | ||
/// </summary> | ||
private volatile uint referenceCount; | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="DispatcherQueueProxyHandler1"/> instance for the input callback and state. | ||
/// </summary> | ||
/// <typeparam name="T">The type of state currently being used.</typeparam> | ||
/// <param name="handler">The input <see cref="DispatcherQueueHandler{TState}"/> callback to enqueue.</param> | ||
/// <param name="state">The input state to capture and pass to the callback.</param> | ||
/// <returns>A pointer to the newly initialized <see cref="DispatcherQueueProxyHandler1"/> instance.</returns> | ||
public static DispatcherQueueProxyHandler1* Create<T>(DispatcherQueueHandler<T> 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<T>; | ||
@this->referenceCount = 1; | ||
|
||
return @this; | ||
} | ||
|
||
/// <summary> | ||
/// Devirtualized API for <c>IUnknown.AddRef()</c>. | ||
/// </summary> | ||
/// <returns>The updated reference count for the current instance.</returns> | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public uint AddRef() | ||
{ | ||
return Interlocked.Increment(ref referenceCount); | ||
} | ||
|
||
/// <summary> | ||
/// Devirtualized API for <c>IUnknown.Release()</c>. | ||
/// </summary> | ||
/// <returns>The updated reference count for the current instance.</returns> | ||
[MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
public uint Release() | ||
{ | ||
uint referenceCount = Interlocked.Decrement(ref this.referenceCount); | ||
|
||
if (referenceCount == 0) | ||
{ | ||
callbackHandle.Free(); | ||
stateHandle.Free(); | ||
|
||
Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this)); | ||
} | ||
|
||
return referenceCount; | ||
} | ||
|
||
/// <summary> | ||
/// A private type with the implementation of the unmanaged methods for <see cref="DispatcherQueueProxyHandler1"/>. | ||
/// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface. | ||
/// </summary> | ||
private static class Impl | ||
{ | ||
/// <summary> | ||
/// Implements <c>IUnknown.QueryInterface(REFIID, void**)</c>. | ||
/// </summary> | ||
[UnmanagedCallersOnly] | ||
public static int QueryInterface(DispatcherQueueProxyHandler1* @this, Guid* riid, void** ppvObject) | ||
{ | ||
if (riid->Equals(GuidOfIUnknown) || | ||
riid->Equals(GuidOfIAgileObject) || | ||
riid->Equals(GuidOfIDispatcherQueueHandler)) | ||
{ | ||
@this->AddRef(); | ||
|
||
*ppvObject = @this; | ||
|
||
return S_OK; | ||
} | ||
|
||
return E_NOINTERFACE; | ||
} | ||
|
||
/// <summary> | ||
/// Implements <c>IUnknown.AddRef()</c>. | ||
/// </summary> | ||
[UnmanagedCallersOnly] | ||
public static uint AddRef(DispatcherQueueProxyHandler1* @this) | ||
{ | ||
return Interlocked.Increment(ref @this->referenceCount); | ||
} | ||
|
||
/// <summary> | ||
/// Implements <c>IUnknown.Release()</c>. | ||
/// </summary> | ||
[UnmanagedCallersOnly] | ||
public static uint Release(DispatcherQueueProxyHandler1* @this) | ||
{ | ||
uint referenceCount = Interlocked.Decrement(ref @this->referenceCount); | ||
|
||
if (referenceCount == 0) | ||
{ | ||
@this->callbackHandle.Free(); | ||
@this->stateHandle.Free(); | ||
|
||
Marshal.FreeHGlobal((IntPtr)@this); | ||
} | ||
|
||
return referenceCount; | ||
} | ||
|
||
/// <summary> | ||
/// Implements <c>IDispatcherQueueHandler.Invoke()</c>. | ||
/// </summary> | ||
[UnmanagedCallersOnly] | ||
public static int Invoke(DispatcherQueueProxyHandler1* @this) | ||
{ | ||
return @this->stub(@this); | ||
} | ||
|
||
/// <summary> | ||
/// Implements <c>IDispatcherQueueHandler.Invoke()</c> from within a generic context. | ||
/// </summary> | ||
public static int Invoke<T>(DispatcherQueueProxyHandler1* @this) | ||
where T : class | ||
{ | ||
object callback = @this->callbackHandle.Target!; | ||
object state = @this->stateHandle.Target!; | ||
|
||
try | ||
{ | ||
Unsafe.As<DispatcherQueueHandler<T>>(callback)(Unsafe.As<T>(state)); | ||
} | ||
catch (Exception e) | ||
{ | ||
ExceptionHelpers.SetErrorInfo(e); | ||
|
||
return ExceptionHelpers.GetHRForException(e); | ||
} | ||
|
||
return S_OK; | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.