From f732b13622f812e9cb487072c78e5af09cdcf4e2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 13:33:40 +0900 Subject: [PATCH] Better WndProc handling --- .../Hooking/WndProcHook/WndProcEventArgs.cs | 144 +++++++++ .../WndProcHook/WndProcEventDelegate.cs | 7 + .../Hooking/WndProcHook/WndProcHookManager.cs | 115 ++++++++ Dalamud/Interface/Internal/DalamudIme.cs | 33 ++- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Interface/Internal/WndProcHookManager.cs | 275 ------------------ 6 files changed, 294 insertions(+), 290 deletions(-) create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcHookManager.cs delete mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs new file mode 100644 index 0000000000..a835f9125f --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs @@ -0,0 +1,144 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Event arguments for , +/// and the manager for individual WndProc hook. +/// +internal sealed unsafe class WndProcEventArgs +{ + private readonly WndProcHookManager owner; + private readonly delegate* unmanaged oldWndProcW; + private readonly WndProcDelegate myWndProc; + + private GCHandle gcHandle; + private bool released; + + /// + /// Initializes a new instance of the class. + /// + /// The owner. + /// The handle of the target window of the message. + /// The viewport ID. + internal WndProcEventArgs(WndProcHookManager owner, HWND hwnd, int viewportId) + { + this.Hwnd = hwnd; + this.owner = owner; + this.ViewportId = viewportId; + this.myWndProc = this.WndProcDetour; + this.oldWndProcW = (delegate* unmanaged)SetWindowLongPtrW( + hwnd, + GWLP.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(this.myWndProc)); + this.gcHandle = GCHandle.Alloc(this); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); + + /// + /// Gets the handle of the target window of the message. + /// + public HWND Hwnd { get; } + + /// + /// Gets the ImGui viewport ID. + /// + public int ViewportId { get; } + + /// + /// Gets or sets the message. + /// + public uint Message { get; set; } + + /// + /// Gets or sets the WPARAM. + /// + public WPARAM WParam { get; set; } + + /// + /// Gets or sets the LPARAM. + /// + public LPARAM LParam { get; set; } + + /// + /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from . + ///
+ public bool SuppressCall { get; set; } + + /// + /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from . + ///
+ public LRESULT ReturnValue { get; set; } + + /// + /// Sets to true and sets . + /// + /// The new return value. + public void SuppressWithValue(LRESULT returnValue) + { + this.ReturnValue = returnValue; + this.SuppressCall = true; + } + + /// + /// Sets to true and sets from the result of + /// . + /// + public void SuppressWithDefault() + { + this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); + this.SuppressCall = true; + } + + /// + internal void InternalRelease() + { + if (this.released) + return; + + this.released = true; + SendMessageTimeoutW(this.Hwnd, WM.WM_NULL, 0, 0, SMTO_ERRORONEXIT, INFINITE, null); + this.FinalRelease(); + } + + private void FinalRelease() + { + if (!this.gcHandle.IsAllocated) + return; + + this.gcHandle.Free(); + SetWindowLongPtrW(this.Hwnd, GWLP.GWLP_WNDPROC, (nint)this.oldWndProcW); + this.owner.OnHookedWindowRemoved(this); + } + + private LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + if (hwnd != this.Hwnd) + return CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.SuppressCall = false; + this.ReturnValue = 0; + this.Message = uMsg; + this.WParam = wParam; + this.LParam = lParam; + this.owner.InvokePreWndProc(this); + + if (!this.SuppressCall) + this.ReturnValue = CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.owner.InvokePostWndProc(this); + + if (uMsg == WM.WM_NCDESTROY || this.released) + this.FinalRelease(); + + return this.ReturnValue; + } +} diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs new file mode 100644 index 0000000000..f753f16cc2 --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs @@ -0,0 +1,7 @@ +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Delegate for overriding WndProc. +/// +/// The arguments. +internal delegate void WndProcEventDelegate(WndProcEventArgs args); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs new file mode 100644 index 0000000000..00934f27f0 --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Manages WndProc hooks for game main window and extra ImGui viewport windows. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class WndProcHookManager : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); + + private readonly Hook dispatchMessageWHook; + private readonly Dictionary wndProcOverrides = new(); + + [ServiceManager.ServiceConstructor] + private unsafe WndProcHookManager() + { + this.dispatchMessageWHook = Hook.FromImport( + null, + "user32.dll", + "DispatchMessageW", + 0, + this.DispatchMessageWDetour); + this.dispatchMessageWHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate nint DispatchMessageWDelegate(MSG* msg); + + /// + /// Called before WndProc. + /// + public event WndProcEventDelegate? PreWndProc; + + /// + /// Called after WndProc. + /// + public event WndProcEventDelegate? PostWndProc; + + /// + public void Dispose() + { + this.dispatchMessageWHook.Dispose(); + foreach (var v in this.wndProcOverrides.Values) + v.InternalRelease(); + this.wndProcOverrides.Clear(); + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePreWndProc(WndProcEventArgs args) + { + try + { + this.PreWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PreWndProc)} error"); + } + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePostWndProc(WndProcEventArgs args) + { + try + { + this.PostWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + } + + /// + /// Removes from the list of known WndProc overrides. + /// + /// Object to remove. + internal void OnHookedWindowRemoved(WndProcEventArgs args) + { + if (!this.dispatchMessageWHook.IsDisposed) + this.wndProcOverrides.Remove(args.Hwnd); + } + + /// + /// Detour for . Used to discover new windows to hook. + /// + /// The message. + /// The original return value. + private unsafe nint DispatchMessageWDetour(MSG* msg) + { + if (!this.wndProcOverrides.ContainsKey(msg->hwnd) + && ImGuiHelpers.FindViewportId(msg->hwnd) is var vpid and >= 0) + { + this.wndProcOverrides[msg->hwnd] = new(this, msg->hwnd, vpid); + } + + return this.dispatchMessageWHook.Original(msg); + } +} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index b3252546a3..9bd9a24983 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -8,6 +8,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -77,6 +78,8 @@ internal static bool ShowCursorInInputText { get { + if (!ImGuiHelpers.IsImGuiInitialized) + return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; ref var textState = ref TextState; @@ -185,7 +188,7 @@ public void ReflectCharacterEncounters(string str) /// Processes window messages. /// /// The arguments. - public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + public void ProcessImeMessage(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) return; @@ -208,11 +211,11 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_STARTCOMPOSITION: - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_COMPOSITION: @@ -222,22 +225,22 @@ public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs ar this.ReplaceCompositionString(hImc, (uint)args.LParam); // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_CONTROL: // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_REQUEST: // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_SETCONTEXT: @@ -298,7 +301,11 @@ private static string ImmGetCompositionString(HIMC hImc, uint comp) return new(data, 0, numBytes / 2); } - private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + private void ReleaseUnmanagedResources() + { + if (ImGuiHelpers.IsImGuiInitialized) + ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + } private void UpdateInputLanguage(HIMC hImc) { @@ -492,8 +499,16 @@ private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformIme } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + { + if (!ImGuiHelpers.IsImGuiInitialized) + { + throw new InvalidOperationException( + $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui."); + } + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + } /// /// Ported from imstb_textedit.h. diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 49dfdb248a..48157fa866 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,6 +15,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; @@ -659,16 +660,13 @@ private void InitScene(IntPtr swapChain) this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; } - private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args) + private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); if (r is not null) - { - args.ReturnValue = r.Value; - args.SuppressCall = true; - } + args.SuppressWithValue(r.Value); - this.dalamudIme.ProcessImeMessage(ref args); + this.dalamudIme.ProcessImeMessage(args); } /* diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs deleted file mode 100644 index 1110ff3876..0000000000 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using Dalamud.Hooking; -using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; - -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Interface.Internal; - -/// -/// A manifestation of "I can't believe this is required". -/// -[ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable -{ - private static readonly ModuleLog Log = new("WPHM"); - - private readonly Hook dispatchMessageWHook; - private readonly Dictionary wndProcNextDict = new(); - private readonly WndProcDelegate wndProcDelegate; - private readonly uint unhookSelfMessage; - private bool disposed; - - [ServiceManager.ServiceConstructor] - private unsafe WndProcHookManager() - { - this.wndProcDelegate = this.WndProcDetour; - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); - this.dispatchMessageWHook.Enable(); - fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}") - this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName); - } - - /// - /// Finalizes an instance of the class. - /// - ~WndProcHookManager() => this.ReleaseUnmanagedResources(); - - /// - /// Delegate for overriding WndProc. - /// - /// The arguments. - public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate nint DispatchMessageWDelegate(ref MSG msg); - - /// - /// Called before WndProc. - /// - public event WndProcOverrideDelegate? PreWndProc; - - /// - /// Called after WndProc. - /// - public event WndProcOverrideDelegate? PostWndProc; - - /// - public void Dispose() - { - this.disposed = true; - this.dispatchMessageWHook.Dispose(); - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - /// - /// Detour for . Used to discover new windows to hook. - /// - /// The message. - /// The original return value. - private unsafe nint DispatchMessageWDetour(ref MSG msg) - { - lock (this.wndProcNextDict) - { - if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 && - !this.wndProcNextDict.ContainsKey(msg.hwnd)) - { - this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW( - msg.hwnd, - GWLP.GWLP_WNDPROC, - Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate)); - } - } - - return this.dispatchMessageWHook.IsDisposed - ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg)) - : this.dispatchMessageWHook.Original(ref msg); - } - - private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) - { - nint nextProc; - lock (this.wndProcNextDict) - { - if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc)) - { - // Something went wrong; prevent crash. Things will, regardless of the effort, break. - return DefWindowProcW(hwnd, uMsg, wParam, lParam); - } - } - - if (uMsg == this.unhookSelfMessage) - { - // Even though this message is dedicated for our processing, - // satisfy the expectations by calling the next window procedure. - var rv = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - - return rv; - } - - var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); - try - { - this.PreWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - - if (!arg.SuppressCall) - { - try - { - arg.ReturnValue = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault"); - } - - try - { - this.PostWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - } - - if (uMsg == WM.WM_NCDESTROY) - { - // The window will cease to exist, once we return. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - } - - return arg.ReturnValue; - } - - private void ReleaseUnmanagedResources() - { - this.disposed = true; - - // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first. - HWND[] windows; - lock (this.wndProcNextDict) - windows = this.wndProcNextDict.Keys.ToArray(); - - // Unregister our hook from all the windows we hooked. - foreach (var v in windows) - SendMessageW(v, this.unhookSelfMessage, default, default); - } - - /// - /// Parameters for . - /// - public ref struct WndProcOverrideEventArgs - { - /// - /// The handle of the target window of the message. - /// - public readonly HWND Hwnd; - - /// - /// The message. - /// - public ref uint Message; - - /// - /// The WPARAM. - /// - public ref WPARAM WParam; - - /// - /// The LPARAM. - /// - public ref LPARAM LParam; - - /// - /// Initializes a new instance of the struct. - /// - /// The handle of the target window of the message. - /// The message. - /// The WPARAM. - /// The LPARAM. - public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam) - { - this.Hwnd = hwnd; - this.LParam = ref lParam; - this.WParam = ref wParam; - this.Message = ref msg; - this.ViewportId = ImGuiHelpers.FindViewportId(hwnd); - } - - /// - /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
- /// Does nothing if changed from . - ///
- public bool SuppressCall { get; set; } - - /// - /// Gets or sets the return value.
- /// Has the return value from next window procedure, if accessed from . - ///
- public LRESULT ReturnValue { get; set; } - - /// - /// Gets the ImGui viewport ID. - /// - public int ViewportId { get; init; } - - /// - /// Gets a value indicating whether this message is for the game window (the first viewport). - /// - public bool IsGameWindow => this.ViewportId == 0; - - /// - /// Sets to true and sets . - /// - /// The new return value. - public void SuppressAndReturn(LRESULT returnValue) - { - this.ReturnValue = returnValue; - this.SuppressCall = true; - } - - /// - /// Sets to true and calls . - /// - public void SuppressWithDefault() - { - this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); - this.SuppressCall = true; - } - } -}