From 8967174cd968d6993401fa706bc7682657127ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:28:12 +0900 Subject: [PATCH] Reimplement clipboard text normalizer to use the correct buffers --- .../Internal/ImGuiClipboardConfig.cs | 232 +++++++++++++++--- .../Interface/Internal/InterfaceManager.cs | 2 - 2 files changed, 193 insertions(+), 41 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index b3302add4..5dc04d736 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -1,4 +1,9 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal; @@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal; /// works for both ImGui and XIV. /// /// -internal static class ImGuiClipboardConfig +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable { - private delegate void SetClipboardTextDelegate(IntPtr userData, string text); - private delegate string GetClipboardTextDelegate(); + private readonly nint clipboardUserDataOriginal; + private readonly delegate* unmanaged setTextOriginal; + private readonly delegate* unmanaged getTextOriginal; - private static SetClipboardTextDelegate? _setTextOriginal = null; - private static GetClipboardTextDelegate? _getTextOriginal = null; + [ServiceManager.ServiceConstructor] + private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) + { + // Effectively waiting for ImGui to become available. + _ = imws; + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); - // These must exist as variables to prevent them from being GC'd - private static SetClipboardTextDelegate? _setText = null; - private static GetClipboardTextDelegate? _getText = null; + var io = ImGui.GetIO(); + this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; + this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.clipboardUserDataOriginal = io.ClipboardUserData; + io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); + io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; + io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + return; - public static void Apply() + [UnmanagedCallersOnly] + static void StaticSetClipboardTextImpl(nint userData, byte* text) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); + + [UnmanagedCallersOnly] + static byte* StaticGetClipboardTextImpl(nint userData) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); + } + + /// + /// Finalizes an instance of the class. + /// + ~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources(); + + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] + private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => + new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() { var io = ImGui.GetIO(); - if (_setTextOriginal == null) - { - _setTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn); - } + if (io.ClipboardUserData == default) + return; - if (_getTextOriginal == null) - { - _getTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn); - } - - _setText = new SetClipboardTextDelegate(SetClipboardText); - _getText = new GetClipboardTextDelegate(GetClipboardText); - - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText); - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText); + GCHandle.FromIntPtr(io.ClipboardUserData).Free(); + io.SetClipboardTextFn = (nint)this.setTextOriginal; + io.GetClipboardTextFn = (nint)this.getTextOriginal; + io.ClipboardUserData = this.clipboardUserDataOriginal; } - public static void Unapply() + private void SetClipboardTextImpl(byte* text) { - var io = ImGui.GetIO(); - if (_setTextOriginal != null) - { - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal); - } - if (_getTextOriginal != null) - { - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal); - } + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); } - private static void SetClipboardText(IntPtr userData, string text) + private byte* GetClipboardTextImpl() { - _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n")); + _ = this.getTextOriginal(this.clipboardUserDataOriginal); + + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.TrimNullTerminator(ref buffer); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + return buffer.Data; } - private static string GetClipboardText() + private static class Utf8Utils { - return _getTextOriginal!().ReplaceLineEndings("\r\n"); + /// + /// Sets from , a null terminated UTF-8 string. + /// + /// The target buffer. It will not contain a null terminator. + /// The pointer to the null-terminated UTF-8 string. + public static void SetFromNullTerminatedBytes(ref ImVectorWrapper buf, byte* psz) + { + var len = 0; + while (psz[len] != 0) + len++; + + buf.Clear(); + buf.AddRange(new Span(psz, len)); + } + + /// + /// Removes the null terminator. + /// + /// The UTF-8 string buffer. + public static void TrimNullTerminator(ref ImVectorWrapper buf) + { + while (buf.Length > 0 && buf[^1] == 0) + buf.LengthUnsafe--; + } + + /// + /// Adds a null terminator to the buffer. + /// + /// The buffer. + public static void AddNullTerminatorIfMissing(ref ImVectorWrapper buf) + { + if (buf.Length > 0 && buf[^1] == 0) + return; + buf.Add(0); + } + + /// + /// Counts the number of bytes for the UTF-8 character. + /// + /// The bytes. + /// Available number of bytes. + /// Number of bytes taken, or -1 if the byte was invalid. + public static int CountBytes(byte* b, int avail) + { + if (avail <= 0) + return 0; + if ((b[0] & 0x80) == 0) + return 1; + if ((b[0] & 0xE0) == 0xC0 && avail >= 2) + return (b[1] & 0xC0) == 0x80 ? 2 : -1; + if ((b[0] & 0xF0) == 0xE0 && avail >= 3) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1; + if ((b[0] & 0xF8) == 0xF0 && avail >= 4) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1; + return -1; + } + + /// + /// Gets the codepoint. + /// + /// The bytes. + /// The result from . + /// The codepoint, or \xFFFD replacement character if failed. + public static int GetCodepoint(byte* b, int cb) => cb switch + { + 1 => b[0], + 2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F), + 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), + 4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), + _ => 0xFFFD, + }; + + /// + /// Normalize the given text for our use case. + /// + /// The buffer. + public static void Normalize(ref ImVectorWrapper buf) + { + for (var i = 0; i < buf.Length;) + { + // Already correct? + if (buf[i] is 0x0D && buf[i + 1] is 0x0A) + { + i += 2; + continue; + } + + var cb = CountBytes(buf.Data + i, buf.Length - i); + var currInt = GetCodepoint(buf.Data + i, cb); + switch (currInt) + { + case 0xFFFF: // Simply invalid + case > char.MaxValue: // ImWchar is same size with char; does not support + case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support + // Replace with \uFFFD in UTF-8: EF BF BD + buf[i++] = 0xEF; + buf.Insert(i++, 0xBF); + buf.Insert(i++, 0xBD); + break; + + // See String.Manipulation.cs: IndexOfNewlineChar. + case '\r': // CR; Carriage Return + case '\n': // LF; Line Feed + case '\f': // FF; Form Feed + buf[i++] = 0x0D; + buf.Insert(i++, 0x0A); + break; + + case '\u0085': // NEL; Next Line + case '\u2028': // LS; Line Separator + case '\u2029': // PS; Paragraph Separator + buf[i++] = 0x0D; + buf[i++] = 0x0A; + break; + + default: + // Not a newline char. + i += cb; + break; + } + } + } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7d164c01f..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -240,7 +240,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.processMessageHook?.Dispose(); }).Wait(); - ImGuiClipboardConfig.Unapply(); this.scene?.Dispose(); } @@ -629,7 +628,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; this.SetupFonts(); - ImGuiClipboardConfig.Apply(); if (!configuration.IsDocking) {