Skip to content

Commit

Permalink
Better WndProc handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Soreepeong committed Dec 17, 2023
1 parent 4be635b commit 0afb3d2
Show file tree
Hide file tree
Showing 6 changed files with 294 additions and 290 deletions.
144 changes: 144 additions & 0 deletions Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Runtime.InteropServices;

using TerraFX.Interop.Windows;

using static TerraFX.Interop.Windows.Windows;

namespace Dalamud.Hooking.WndProcHook;

/// <summary>
/// Event arguments for <see cref="WndProcEventDelegate"/>,
/// and the manager for individual WndProc hook.
/// </summary>
internal sealed unsafe class WndProcEventArgs
{
private readonly WndProcHookManager owner;
private readonly delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT> oldWndProcW;
private readonly WndProcDelegate myWndProc;

private GCHandle gcHandle;
private bool released;

/// <summary>
/// Initializes a new instance of the <see cref="WndProcEventArgs"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
/// <param name="hwnd">The handle of the target window of the message.</param>
/// <param name="viewportId">The viewport ID.</param>
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<HWND, uint, WPARAM, LPARAM, LRESULT>)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);

/// <summary>
/// Gets the handle of the target window of the message.
/// </summary>
public HWND Hwnd { get; }

/// <summary>
/// Gets the ImGui viewport ID.
/// </summary>
public int ViewportId { get; }

/// <summary>
/// Gets or sets the message.
/// </summary>
public uint Message { get; set; }

/// <summary>
/// Gets or sets the WPARAM.
/// </summary>
public WPARAM WParam { get; set; }

/// <summary>
/// Gets or sets the LPARAM.
/// </summary>
public LPARAM LParam { get; set; }

/// <summary>
/// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.<br />
/// Does nothing if changed from <see cref="WndProcHookManager.PostWndProc"/>.
/// </summary>
public bool SuppressCall { get; set; }

/// <summary>
/// Gets or sets the return value.<br />
/// Has the return value from next window procedure, if accessed from <see cref="WndProcHookManager.PostWndProc"/>.
/// </summary>
public LRESULT ReturnValue { get; set; }

/// <summary>
/// Sets <see cref="SuppressCall"/> to <c>true</c> and sets <see cref="ReturnValue"/>.
/// </summary>
/// <param name="returnValue">The new return value.</param>
public void SuppressWithValue(LRESULT returnValue)
{
this.ReturnValue = returnValue;
this.SuppressCall = true;
}

/// <summary>
/// Sets <see cref="SuppressCall"/> to <c>true</c> and sets <see cref="ReturnValue"/> from the result of
/// <see cref="DefWindowProcW"/>.
/// </summary>
public void SuppressWithDefault()
{
this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam);
this.SuppressCall = true;
}

/// <inheritdoc cref="IDisposable.Dispose"/>
internal void InternalRelease()
{
if (this.released)
return;

this.released = true;
SendMessageW(this.Hwnd, WM.WM_NULL, 0, 0);
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;
}
}
7 changes: 7 additions & 0 deletions Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Dalamud.Hooking.WndProcHook;

/// <summary>
/// Delegate for overriding WndProc.
/// </summary>
/// <param name="args">The arguments.</param>
internal delegate void WndProcEventDelegate(WndProcEventArgs args);
115 changes: 115 additions & 0 deletions Dalamud/Hooking/WndProcHook/WndProcHookManager.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Manages WndProc hooks for game main window and extra ImGui viewport windows.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class WndProcHookManager : IServiceType, IDisposable
{
private static readonly ModuleLog Log = new(nameof(WndProcHookManager));

private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = new();

[ServiceManager.ServiceConstructor]
private unsafe WndProcHookManager()
{
this.dispatchMessageWHook = Hook<DispatchMessageWDelegate>.FromImport(
null,
"user32.dll",
"DispatchMessageW",
0,
this.DispatchMessageWDetour);
this.dispatchMessageWHook.Enable();
}

[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private unsafe delegate nint DispatchMessageWDelegate(MSG* msg);

/// <summary>
/// Called before WndProc.
/// </summary>
public event WndProcEventDelegate? PreWndProc;

/// <summary>
/// Called after WndProc.
/// </summary>
public event WndProcEventDelegate? PostWndProc;

/// <inheritdoc/>
public void Dispose()
{
this.dispatchMessageWHook.Dispose();
foreach (var v in this.wndProcOverrides.Values)
v.InternalRelease();
this.wndProcOverrides.Clear();
}

/// <summary>
/// Invokes <see cref="PreWndProc"/>.
/// </summary>
/// <param name="args">The arguments.</param>
internal void InvokePreWndProc(WndProcEventArgs args)
{
try
{
this.PreWndProc?.Invoke(args);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(this.PreWndProc)} error");
}
}

/// <summary>
/// Invokes <see cref="PostWndProc"/>.
/// </summary>
/// <param name="args">The arguments.</param>
internal void InvokePostWndProc(WndProcEventArgs args)
{
try
{
this.PostWndProc?.Invoke(args);
}
catch (Exception e)
{
Log.Error(e, $"{nameof(this.PostWndProc)} error");
}
}

/// <summary>
/// Removes <paramref name="args"/> from the list of known WndProc overrides.
/// </summary>
/// <param name="args">Object to remove.</param>
internal void OnHookedWindowRemoved(WndProcEventArgs args)
{
if (!this.dispatchMessageWHook.IsDisposed)
this.wndProcOverrides.Remove(args.Hwnd);
}

/// <summary>
/// Detour for <see cref="DispatchMessageW"/>. Used to discover new windows to hook.
/// </summary>
/// <param name="msg">The message.</param>
/// <returns>The original return value.</returns>
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);
}
}
33 changes: 24 additions & 9 deletions Dalamud/Interface/Internal/DalamudIme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -185,7 +188,7 @@ public void ReflectCharacterEncounters(string str)
/// Processes window messages.
/// </summary>
/// <param name="args">The arguments.</param>
public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args)
public void ProcessImeMessage(WndProcEventArgs args)
{
if (!ImGuiHelpers.IsImGuiInitialized)
return;
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}

/// <summary>
/// Ported from imstb_textedit.h.
Expand Down
10 changes: 4 additions & 6 deletions Dalamud/Interface/Internal/InterfaceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/*
Expand Down
Loading

0 comments on commit 0afb3d2

Please sign in to comment.