Skip to content

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
wants to merge 13 commits into
base: winui
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Title>Windows Community Toolkit - Common (UWP)</Title>
<Description>This package includes code only helpers such as Color conversion tool, Storage file handling, a Stream helper class, SystemInformation helpers, etc.</Description>
<PackageTags>Storage;File;Folder;Color;Conversion;Stream;Helpers;Extensions;System;Information</PackageTags>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace CommunityToolkit.WinUI
/// <summary>
/// Helpers for executing code in a <see cref="DispatcherQueue"/>.
/// </summary>
public static class DispatcherQueueExtensions
public static partial class DispatcherQueueExtensions
{
/// <summary>
/// Invokes a given function on the target <see cref="DispatcherQueue"/> and returns a
Expand Down
153 changes: 153 additions & 0 deletions CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions{T}.cs
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;
}
}
}
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;
}
}
}
}
Loading