Skip to content

Commit f19c1f1

Browse files
committed
Added DispatcherQueue.TryEnqueue<TState> extensions
Fixes microsoft/microsoft-ui-xaml#3321
1 parent aff1fb0 commit f19c1f1

File tree

3 files changed

+307
-1
lines changed

3 files changed

+307
-1
lines changed

CommunityToolkit.WinUI/CommunityToolkit.WinUI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<Description>This package includes code only helpers such as Colors conversion tool, Storage file handling, a Stream helper class, etc.</Description>
77
<PackageTags>Windows;Community;Toolkit;WCT;UWP</PackageTags>
88
<LangVersion>9.0</LangVersion>
9+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
910
</PropertyGroup>
1011

1112
<ItemGroup>

CommunityToolkit.WinUI/Extensions/DispatcherQueueExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace CommunityToolkit.WinUI
1414
/// <summary>
1515
/// Helpers for executing code in a <see cref="DispatcherQueue"/>.
1616
/// </summary>
17-
public static class DispatcherQueueExtensions
17+
public static partial class DispatcherQueueExtensions
1818
{
1919
/// <summary>
2020
/// Invokes a given function on the target <see cref="DispatcherQueue"/> and returns a
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Threading;
9+
using Microsoft.UI.Dispatching;
10+
using WinRT;
11+
12+
#nullable enable
13+
14+
#pragma warning disable SA1000, SA1023
15+
16+
namespace CommunityToolkit.WinUI
17+
{
18+
/// <summary>
19+
/// A callback that will be executed on the <see cref="DispatcherQueue"/> thread.
20+
/// </summary>
21+
/// <typeparam name="TState">The type of state to receive as input.</typeparam>
22+
/// <param name="state">The input state for the callback.</param>
23+
public delegate void DispatcherQueueHandler<in TState>(TState state)
24+
where TState : class;
25+
26+
/// <summary>
27+
/// Helpers for executing code in a <see cref="DispatcherQueue"/>.
28+
/// </summary>
29+
public static partial class DispatcherQueueExtensions
30+
{
31+
/// <summary>
32+
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it.
33+
/// </summary>
34+
/// <typeparam name="TState">The type of state to capture.</typeparam>
35+
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param>
36+
/// <param name="callback">The input <see cref="DispatcherQueueHandler{TState}"/> callback to enqueue.</param>
37+
/// <param name="state">The input state to capture and pass to the callback.</param>
38+
/// <returns>Whether or not the task was added to the queue.</returns>
39+
public static unsafe bool TryEnqueue<TState>(this DispatcherQueue dispatcherQueue, DispatcherQueueHandler<TState> callback, TState state)
40+
where TState : class
41+
{
42+
IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr;
43+
DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state);
44+
45+
byte result;
46+
47+
try
48+
{
49+
_ = dispatcherQueuePtr->TryEnqueue(dispatcherQueueHandlerPtr, &result);
50+
51+
GC.KeepAlive(dispatcherQueue);
52+
}
53+
finally
54+
{
55+
dispatcherQueueHandlerPtr->Release();
56+
}
57+
58+
return result == 0;
59+
}
60+
61+
/// <summary>
62+
/// Adds a task to the <see cref="DispatcherQueue"/> which will be executed on the thread associated with it.
63+
/// </summary>
64+
/// <typeparam name="TState">The type of state to capture.</typeparam>
65+
/// <param name="dispatcherQueue">The target <see cref="DispatcherQueue"/> to invoke the code on.</param>
66+
/// <param name="priority"> The desired priority for the callback to schedule.</param>
67+
/// <param name="callback">The input <see cref="DispatcherQueueHandler{TState}"/> callback to enqueue.</param>
68+
/// <param name="state">The input state to capture and pass to the callback.</param>
69+
/// <returns>Whether or not the task was added to the queue.</returns>
70+
public static unsafe bool TryEnqueue<TState>(this DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority, DispatcherQueueHandler<TState> callback, TState state)
71+
where TState : class
72+
{
73+
IDispatcherQueue* dispatcherQueuePtr = (IDispatcherQueue*)((IWinRTObject)dispatcherQueue).NativeObject.ThisPtr;
74+
DispatcherQueueProxyHandler* dispatcherQueueHandlerPtr = DispatcherQueueProxyHandler.Create(callback, state);
75+
76+
byte result;
77+
78+
try
79+
{
80+
_ = dispatcherQueuePtr->TryEnqueueWithPriority(priority, dispatcherQueueHandlerPtr, &result);
81+
82+
GC.KeepAlive(dispatcherQueue);
83+
}
84+
finally
85+
{
86+
dispatcherQueueHandlerPtr->Release();
87+
}
88+
89+
return result == 0;
90+
}
91+
92+
/// <summary>
93+
/// A struct mapping the native WinRT <c>IDispatcherQueue</c> interface.
94+
/// </summary>
95+
private unsafe struct IDispatcherQueue
96+
{
97+
private readonly void** lpVtbl;
98+
99+
/// <summary>
100+
/// Native API for <see cref="DispatcherQueue.TryEnqueue(DispatcherQueueHandler)"/>.
101+
/// </summary>
102+
/// <param name="callback">A pointer to an <c>IDispatcherQueueHandler</c> object.</param>
103+
/// <param name="result">The result of the operation (the <see cref="bool"/> WinRT retval).</param>
104+
/// <returns>The HRESULT for the operation.</returns>
105+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106+
public int TryEnqueue(void* callback, byte* result)
107+
{
108+
return ((delegate* unmanaged<IDispatcherQueue*, void*, byte*, int>)lpVtbl[7])((IDispatcherQueue*)Unsafe.AsPointer(ref this), callback, result);
109+
}
110+
111+
/// <summary>
112+
/// Native API for <see cref="DispatcherQueue.TryEnqueue(DispatcherQueuePriority, DispatcherQueueHandler)"/>.
113+
/// </summary>
114+
/// <param name="priority">The priority for the input callback.</param>
115+
/// <param name="callback">A pointer to an <c>IDispatcherQueueHandler</c> object.</param>
116+
/// <param name="result">The result of the operation (the <see cref="bool"/> WinRT retval).</param>
117+
/// <returns>The HRESULT for the operation.</returns>
118+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
119+
public int TryEnqueueWithPriority(DispatcherQueuePriority priority, void* callback, byte* result)
120+
{
121+
return ((delegate* unmanaged<IDispatcherQueue*, DispatcherQueuePriority, void*, byte*, int>)lpVtbl[8])((IDispatcherQueue*)Unsafe.AsPointer(ref this), priority, callback, result);
122+
}
123+
}
124+
125+
/// <summary>
126+
/// A custom <c>IDispatcherQueueHandler</c> object, that internally stores a captured <see cref="DispatcherQueueHandler{TState}"/> instance
127+
/// and the input captured state. This allows consumers to enqueue a state and a cached stateless delegate without any managed allocations.
128+
/// </summary>
129+
private unsafe struct DispatcherQueueProxyHandler
130+
{
131+
private const int S_OK = 0;
132+
private const int E_NOINTERFACE = unchecked((int)0x80004002);
133+
134+
private static readonly Guid IUnknown = new(0x00000000, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46);
135+
private static readonly Guid IAgileObject = new(0x94EA2B94, 0xE9CC, 0x49E0, 0xC0, 0xFF, 0xEE, 0x64, 0xCA, 0x8F, 0x5B, 0x90);
136+
private static readonly Guid IDispatcherQueueHandler = new(0x2E0872A9, 0x4E29, 0x5F14, 0xB6, 0x88, 0xFB, 0x96, 0xD5, 0xF9, 0xD5, 0xF8);
137+
138+
/// <summary>
139+
/// The shared vtable pointer for <see cref="DispatcherQueueProxyHandler"/> instances.
140+
/// </summary>
141+
private static readonly void** Vtbl = InitVtbl();
142+
143+
/// <summary>
144+
/// Setups the vtable pointer for <see cref="DispatcherQueueProxyHandler"/>.
145+
/// </summary>
146+
/// <returns>The initialized vtable pointer for <see cref="DispatcherQueueProxyHandler"/>.</returns>
147+
/// <remarks>
148+
/// The vtable itself is allocated with <see cref="RuntimeHelpers.AllocateTypeAssociatedMemory(Type, int)"/>,
149+
/// which allocates memory in the high frequency heap associated with the input runtime type. This will be
150+
/// automatically cleaned up when the type is unloaded, so there is no need to ever manually free this memory.
151+
/// </remarks>
152+
private static void** InitVtbl()
153+
{
154+
void** lpVtbl = (void**)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(DispatcherQueueProxyHandler), sizeof(void*) * 4);
155+
156+
lpVtbl[0] = (delegate* unmanaged<DispatcherQueueProxyHandler*, Guid*, void**, int>)&Impl.QueryInterface;
157+
lpVtbl[1] = (delegate* unmanaged<DispatcherQueueProxyHandler*, uint>)&Impl.AddRef;
158+
lpVtbl[2] = (delegate* unmanaged<DispatcherQueueProxyHandler*, uint>)&Impl.Release;
159+
lpVtbl[3] = (delegate* unmanaged<DispatcherQueueProxyHandler*, int>)&Impl.Invoke;
160+
161+
return lpVtbl;
162+
}
163+
164+
/// <summary>
165+
/// The vtable pointer for the current instance.
166+
/// </summary>
167+
private void** lpVtbl;
168+
169+
/// <summary>
170+
/// The <see cref="GCHandle"/> to the captured <see cref="DispatcherQueueHandler{TState}"/> (for some unknown <c>TState</c> type).
171+
/// </summary>
172+
private GCHandle callbackHandle;
173+
174+
/// <summary>
175+
/// The <see cref="GCHandle"/> to the captured state (with an unknown <c>TState</c> type).
176+
/// </summary>
177+
private GCHandle stateHandle;
178+
179+
/// <summary>
180+
/// The current reference count for the object (from <c>IUnknown</c>).
181+
/// </summary>
182+
private volatile uint referenceCount;
183+
184+
/// <summary>
185+
/// Creates a new <see cref="DispatcherQueueProxyHandler"/> instance for the input callback and state.
186+
/// </summary>
187+
/// <typeparam name="TState">The type of state to capture.</typeparam>
188+
/// <param name="handler">The input <see cref="DispatcherQueueHandler{TState}"/> callback to enqueue.</param>
189+
/// <param name="state">The input state to capture and pass to the callback.</param>
190+
/// <returns>A pointer to the newly initialized <see cref="DispatcherQueueProxyHandler"/> instance.</returns>
191+
public static DispatcherQueueProxyHandler* Create<TState>(DispatcherQueueHandler<TState> handler, TState state)
192+
where TState : class
193+
{
194+
DispatcherQueueProxyHandler* @this = (DispatcherQueueProxyHandler*)Marshal.AllocHGlobal(sizeof(DispatcherQueueProxyHandler));
195+
196+
@this->lpVtbl = Vtbl;
197+
@this->callbackHandle = GCHandle.Alloc(handler);
198+
@this->stateHandle = GCHandle.Alloc(state);
199+
@this->referenceCount = 1;
200+
201+
return @this;
202+
}
203+
204+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
205+
public uint AddRef()
206+
{
207+
return Interlocked.Increment(ref referenceCount);
208+
}
209+
210+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
211+
public uint Release()
212+
{
213+
uint referenceCount = Interlocked.Decrement(ref this.referenceCount);
214+
215+
if (referenceCount == 0)
216+
{
217+
Marshal.FreeHGlobal((IntPtr)Unsafe.AsPointer(ref this));
218+
}
219+
220+
return referenceCount;
221+
}
222+
223+
/// <summary>
224+
/// A private type with the implementation of the unmanaged methods for <see cref="DispatcherQueueProxyHandler"/>.
225+
/// These methods will be set into the shared vtable and invoked by WinRT from the object passed to it as an interface.
226+
/// </summary>
227+
private static class Impl
228+
{
229+
/// <summary>
230+
/// Implements <c>IUnknown.QueryInterface(REFIID, void**)</c>.
231+
/// </summary>
232+
[UnmanagedCallersOnly]
233+
public static int QueryInterface(DispatcherQueueProxyHandler* @this, Guid* riid, void** ppvObject)
234+
{
235+
if (riid->Equals(IUnknown) ||
236+
riid->Equals(IAgileObject) ||
237+
riid->Equals(IDispatcherQueueHandler))
238+
{
239+
@this->AddRef();
240+
241+
*ppvObject = @this;
242+
243+
return S_OK;
244+
}
245+
246+
return E_NOINTERFACE;
247+
}
248+
249+
/// <summary>
250+
/// Implements <c>IUnknown.AddRef()</c>.
251+
/// </summary>
252+
[UnmanagedCallersOnly]
253+
public static uint AddRef(DispatcherQueueProxyHandler* @this)
254+
{
255+
return Interlocked.Increment(ref @this->referenceCount);
256+
}
257+
258+
/// <summary>
259+
/// Implements <c>IUnknown.Release()</c>.
260+
/// </summary>
261+
[UnmanagedCallersOnly]
262+
public static uint Release(DispatcherQueueProxyHandler* @this)
263+
{
264+
uint referenceCount = Interlocked.Decrement(ref @this->referenceCount);
265+
266+
if (referenceCount == 0)
267+
{
268+
@this->callbackHandle.Free();
269+
@this->stateHandle.Free();
270+
271+
Marshal.FreeHGlobal((IntPtr)@this);
272+
}
273+
274+
return referenceCount;
275+
}
276+
277+
/// <summary>
278+
/// Implements <c>IDispatcherQueueHandler.Invoke()</c>.
279+
/// </summary>
280+
[UnmanagedCallersOnly]
281+
public static int Invoke(DispatcherQueueProxyHandler* @this)
282+
{
283+
object callback = @this->callbackHandle.Target!;
284+
object state = @this->stateHandle.Target!;
285+
286+
try
287+
{
288+
// We do an unsafe cast here to treat the captured delegate as if the contravariant
289+
// input type was actually declared as covariant. This is valid because the type
290+
// parameter is constrained to be a reference type, and due to how the proxy handler
291+
// is constructed we know that the captured state will always match the actual type
292+
// of the captured handler at this point. This lets this whole method work without the
293+
// need to make the proxy type itself generic, so without knowing the actual type argument.
294+
Unsafe.As<DispatcherQueueHandler<object>>(callback)(state);
295+
}
296+
catch
297+
{
298+
}
299+
300+
return S_OK;
301+
}
302+
}
303+
}
304+
}
305+
}

0 commit comments

Comments
 (0)