Skip to content

Instantly share code, notes, and snippets.

@anonhostpi
Created October 21, 2023 06:23
Show Gist options
  • Save anonhostpi/56b3da943138bcb0307d701654c5c9a1 to your computer and use it in GitHub Desktop.
Save anonhostpi/56b3da943138bcb0307d701654c5c9a1 to your computer and use it in GitHub Desktop.
Avalonia.Threading.Dispatcher Testing
<#
$Dispatcher = {
$App = @{}
& {
$builder = [Avalonia.AppBuilder]::Configure[Avalonia.Application]()
$builder = [Avalonia.AppBuilderDesktopExtensions]::UsePlatformDetect( $builder )
$App.Lifetime = [Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime]::new()
$App.Lifetime.ShutdownMode = [Avalonia.Controls.ShutdownMode]::OnExplicitShutdown
$builder = $builder.SetupWithLifetime( $App.Lifetime )
# Return early
$App.Instance = $builder.Instance
}
# Return the UI thread dispatcher
[Avalonia.Threading.Dispatcher]::UIThread
$App.TokenSource = [System.Threading.CancellationTokenSource]::new()
[Avalonia.Controls.DesktopApplicationExtensions]::Run( $App.Instance, $App.TokenSource.Token ) | Out-Null
}
#>
Import-Package Avalonia.Win32
$Dispatcher = & {
# Win32 API Window Procedure (WndProc) which we will use to process Win32 API messages
$MessageProcessor = & {
# Reasons for chunking up the body of $MessageProcessor:
# - Allow us to inject the ManagedThreadId into the body
$body = @(
{
param(
[System.IntPtr] $hWnd,
[uint] $msg,
[System.IntPtr] $wParam,
[System.IntPtr] $lParam
)
}.ToString(),
# Used to get the dispatcher and dispatcherImpl
"`$id = $($ThreadController.Thread.ManagedThreadId)",
# $_dispatcher and $_dispatcherImpl
{
$_threads = if( $Threads ){
$Threads
} else {
(Get-Module -Name New-DispatchThread).Invoke({ $threads })
}
$_dispatcher = ($_threads.GetEnumerator() | Where-Object {
$_.Value.Thread.ManagedThreadId -eq $id
}).Value.Dispatcher
$_dispatcherImpl = &{
$_prop = $_dispatcher.GetType().GetProperty(
"_controlledImpl",
[System.Reflection.BindingFlags]::NonPublic `
-bor [System.Reflection.BindingFlags]::Instance
)
$_prop.GetValue( $_dispatcher )
}
}.ToString(),
# $win32
{
$win32 = @{
"assembly" = [Avalonia.Win32PlatformOptions].Assembly
}
$win32.unmanaged = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "UnmanagedMethods" }
$win32.WindowsMessage = $win32.unmanaged.GetMember("WindowsMessage")
$win32.DispatcherImpl = $_dispatcherImpl.GetType()
# $win32.assembly.GetTypes() |
# Where-Object { $_.Name -eq "Win32DispatcherImpl" }
$win32.Platform = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "Win32Platform" }
$signals = @{
"WNDCLASSEX" = [Activator]::CreateInstance(
$win32.unmanaged.GetMember("WNDCLASSEX")
)
"WM_DISPATCH_WORK_ITEM" = ($win32.WindowsMessage.GetEnumValues() |
Where-Object { $_.ToString() -eq "WM_USER" })[0].value__
"WM_QUERYENDSESSION" = ($win32.WindowsMessage.GetEnumValues() |
Where-Object { $_.ToString() -eq "WM_QUERYENDSESSION" })[0].value__
"WM_SETTINGCHANGE" = ($win32.WindowsMessage.GetEnumValues() |
Where-Object { $_.ToString() -eq "WM_SETTINGCHANGE" })[0].value__
"WM_TIMER" = ($win32.WindowsMessage.GetEnumValues() |
Where-Object { $_.ToString() -eq "WM_TIMER" })[0].value__
"SignalW" = $win32.DispatcherImpl.GetField(
"SignalW",
[System.Reflection.BindingFlags]::NonPublic `
-bor [System.Reflection.BindingFlags]::Static
).GetValue($null) # is 0xDEADBEAF
"SignalL" = $win32.DispatcherImpl.GetField(
"SignalL",
[System.Reflection.BindingFlags]::NonPublic `
-bor [System.Reflection.BindingFlags]::Static
).GetValue($null) # is 0x12345678
"TIMERID_DISPATCHER" = $win32.Platform.GetField(
"TIMERID_DISPATCHER",
[System.Reflection.BindingFlags]::NonPublic `
-bor [System.Reflection.BindingFlags]::Static
).GetValue($null) # is 1
}
}.ToString(),
# Actual body
{
if ($msg -eq $signals.WM_DISPATCH_WORK_ITEM `
-and $wParam.ToInt64() -eq $signals.SignalW `
-and $lParam.ToInt64() -eq $signals.SignalL
){
$_dispatcherImpl?.DispatchWorkItem()
}
<#
# Lifecycle control for Avalonia.Platform. Not super applicable to dispatcher
# - WM_QUERYENDSESSION means there is a request to close the application or shutdown the computer
# - Unlike WM_ENDSESSION, WM_QUERYENDSESSION is a request and can be denied
if ($msg -eq $signals.WM_QUERYENDSESSION)
{
if ($ShutdownRequested -ne $null)
{
$e = [ShutdownRequestedEventArgs]::new()
$ShutdownRequested.Invoke($null, $e)
if($e.Cancel)
{
# If we deny the shutdown, return C-equivalent of false
return [System.IntPtr]::Zero
}
}
}
#>
<#
# System Settings Changed detection for Avalonia.Platform. Not super applicable to dispatcher
# - Is emitted whenever SystemParametersInfoA changes a system-wide setting
# - see: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange
# - see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa
if( $msg -eq $signals.WM_SETTINGCHANGE `
-and $PlatformSettings -is [Win32PlatformSettings] $win32PlatformSettings
){
$changedSetting = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($lParam)
if ($changedSetting -eq "ImmersiveColorSet" # dark/light mode
-or $changedSetting -eq "WindowsThemeElement") # high contrast mode
{
$win32PlatformSettings.OnColorValuesChanged()
}
}
#>
if( $msg -eq $signals.WM_TIMER )
{
if( $wParam -eq $signals.TIMERID_DISPATCHER )
{
if( $_dispatcherImpl -ne $null )
{
$_dispatcherImpl.FireTimer()
}
}
}
# Handle tray icon messages
# TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam);
# if we don't return anything, the message is passed to the default message processor
return $win32.unmanaged::DefWindowProc($hWnd, $msg, $wParam, $lParam);
}.ToString()
) -join "`n"
$body = {
param(
[System.IntPtr] $hWnd,
[uint] $msg,
[System.IntPtr] $wParam,
[System.IntPtr] $lParam
)
Write-Host "MessageProcessor:" $hWnd $msg $wParam $lParam
}
[scriptblock]::create($body)
}
$constructor = [Avalonia.Threading.Dispatcher].
GetConstructor(
[System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::NonPublic,
$null,
[type[]]@(
[Avalonia.Threading.IDispatcherImpl]
),
$null
)
$win32 = @{
"assembly" = [Avalonia.Win32PlatformOptions].Assembly
}
$win32.unmanaged = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "UnmanagedMethods" }
$win32.WNDCLASSEX = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "WNDCLASSEX" }
$win32.DispatcherImpl = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "Win32DispatcherImpl" }
# Avalonia.Win32.Interop.UnmanagedMethods+WndProc
$win32.WndProc = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "WndProc" }
$win32.MainHandle = $win32.unmanaged::GetModuleHandle($null)
$win32.MesageHandle = & {
$wnd_class_ex = [Activator]::CreateInstance($win32.WNDCLASSEX)
$wnd_class_ex.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($wnd_class_ex)
$wnd_class_ex.lpfnWndProc = $MessageProcessor -As $win32.WndProc
$wnd_class_ex.hInstance = $win32.MainHandle
$wnd_class_ex.lpszClassName = "AvaloniaMessageWindow " + [System.Guid]::NewGuid().ToString()
$atom = $win32.unmanaged::RegisterClassEx([ref]$wnd_class_ex)
if( $atom -eq 0 )
{
$global:_error = [System.ComponentModel.Win32Exception]::new()
throw $_error
}
$win32.unmanaged::CreateWindowEx(
0, # dwExStyle
$atom, # lpClassName
[System.IntPtr]::Zero, # lpWindowName
0, # dwStyle
0, # x
0, # y
0, # nWidth
0, # nHeight
[System.IntPtr]::Zero, # hWndParent
[System.IntPtr]::Zero, # hMenu
[System.IntPtr]::Zero, # hInstance
[System.IntPtr]::Zero # lpParam
)
}
$dispatcher_impl = $win32.DispatcherImpl.GetConstructor([System.IntPtr]).Invoke(@($win32.MesageHandle))
$constructor.Invoke(@($dispatcher_impl))
}
$win32 = @{
"assembly" = [Avalonia.Win32PlatformOptions].Assembly
}
$win32.unmanaged = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "UnmanagedMethods" }
$win32.WNDCLASSEX = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "WNDCLASSEX" }
$win32.DispatcherImpl = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "Win32DispatcherImpl" }
# Avalonia.Win32.Interop.UnmanagedMethods+WndProc
$win32.WndProc = $win32.assembly.GetTypes() |
Where-Object { $_.Name -eq "WndProc" }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment