Skip to content

fix(win): ANSI-маршалинг ToUnicodeEx, Backspace в буфере, клик мыши при выделении - #43

Open
MikeSviblov wants to merge 3 commits into
rashn:mainfrom
MikeSviblov:fix/win-unicode-marshaling-backspace
Open

MikeSviblov wants to merge 3 commits into
rashn:mainfrom
MikeSviblov:fix/win-unicode-marshaling-backspace

Conversation

@MikeSviblov

Copy link
Copy Markdown

Два бага Windows-беты, пойманные живым тестированием на Windows 11 24H2 (ANSI=cp1252). Нашёл их, пока готовил греческую часть #42, но от неё они не зависят — это текущее поведение main.

1. ToUnicodeEx маршалился как ANSI

У DllImport в Win32.cs не указан CharSet → дефолтный CharSet.Ansi. Для [Out] char[] pwszBuff это означает нативный буфер 1 байт на символ, тогда как ToUnicodeEx пишет UTF-16 и считает cchBuff количеством WCHAR. Два следствия:

а) Ручная конверсия кириллицы не работает вовсе. Обратная ANSI-конвертация буфера возвращает младший байт каждого WCHAR: вылетает (U+0432 U+044B U+043B U+0435 U+0442 U+0430 U+0435 U+0442) превращается в 2K;5B05B. На системах с ANSI=cp1251 низкобайтовая часть случайно совпадает с кириллицей — вероятно, поэтому баг не всплывал у русскоязычных тестеров; на cp1252/UTF-8 он проявляется сразу.

б) Порча кучи. API может легально записать до cchBuff WCHAR = 16 байт в 8-байтовый блок. У меня это дважды роняло приложение при печати (0xc0000374 STATUS_HEAP_CORRUPTION). Полный дамп (WER LocalDumps) показывает падение ровно при освобождении этого буфера:

MngdNativeArrayMarshaler.ClearNative(...)        ← heap corruption обнаружена здесь
Win32.ToUnicodeEx(UInt32, UInt32, Byte[], Char[], Int32, UInt32, IntPtr)
KeyMapper.TranslateIn(TypedKey, IntPtr)
KeyMapper.ConvertWord(...)
Converter.ConvertLastWord(KeystrokeBuffer)
TrayIcon.WindowProc(...)

Фикс: CharSet.Unicode у ToUnicodeEx (массив маршалится как WCHAR — размер и содержимое совпадают с контрактом API), той же природы латентный баг у GetModuleHandleW (ANSI-строка в W-функцию; сейчас вызывается только с null), и буфер расширен до 16 WCHAR как запас под многосимвольный вывод dead keys.

2. Backspace не отражался в буфере клавиш

VK_BACK — не typing key и не word boundary, поэтому ручное удаление не убирало клавишу из KeystrokeBuffer, буфер расходился с экраном, и конверсия перепечатывала стёртые опечатки: dsk, + Backspace + допечатка давали «вылбылетает» вместо «вылетает». macOS-монитор удаление обрабатывает (та же семья, что #21) — порт догнан: Backspace → PopLast, Ctrl+Backspace (удаление слова) → Reset, в обоих случаях сбрасывается отложенный reconvert.

Проверка

  • Юнит-тесты: 30/30 на реальном Windows (без SendInputTests — им нужен интерактивный десктоп), + 2 новых на PopLast.
  • Живьём на Win11 24H2: до фикса — 2K;5B05B и два краша за 5 минут печати; после — dsktnftn → «вылетает», сценарий с опечаткой+Backspace корректен, крашей нет.
  • Поведение macOS-версии не затронуто (диff только в windows/).

🤖 Generated with Claude Code

https://claude.ai/code/session_01PZjjdW8bam8gQ872kmquy9

MikeSviblov and others added 3 commits October 23, 2026 11:15
…порча кучи

У DllImport для ToUnicodeEx не был указан CharSet → дефолтный ANSI:
нативный pwszBuff выделялся по 1 байту на символ, а API пишет UTF-16
(2 байта), считая cchBuff количеством WCHAR. Два следствия:

1. Любой многосимвольный вывод (dead keys) переполнял буфер — heap
   corruption 0xc0000374, падение в MngdNativeArrayMarshaler.ClearNative
   (подтверждено полным дампом: стек ConvertLastWord → ConvertWord →
   TranslateIn → ToUnicodeEx).
2. Обратная ANSI-конвертация буфера возвращала младший байт каждого
   WCHAR: «вылетает» (0432 044B 043B 0435 0442 0430 0435 0442)
   превращалось в "2K;5B05B". На системах с ANSI=cp1252 ручная
   конверсия кириллицы не работала вовсе.

Фикс: CharSet.Unicode у ToUnicodeEx (char[] маршалится как WCHAR,
размер и содержимое совпадают с контрактом API), той же природы
латентный баг у GetModuleHandleW (ANSI-строка в W-функцию), буфер
расширен до 16 WCHAR как запас под композицию dead keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZjjdW8bam8gQ872kmquy9
…а стёртое

Ручной Backspace не убирал последнюю клавишу из KeystrokeBuffer (он не
typing key и не word boundary), буфер расходился с экраном, и конверсия
печатала и стёртые опечатки: «dsk,» + Backspace + «sktnftn» давало
«вылбылетает» вместо «вылетает». macOS-монитор удаление обрабатывает
(issue rashn#21) — паритет: Backspace → PopLast, Ctrl+Backspace (удаление
слова) → Reset, в обоих случаях отменяется отложенный reconvert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZjjdW8bam8gQ872kmquy9
…м последнего слова

Выделение мышью не очищало KeystrokeBuffer, и триггер при непустом
буфере уходил в ConvertLastWord: его backspace'ы первым нажатием съедали
всё выделение, а повторный тап шёл по циклу кандидатов («ghbdtn hello
руддщ» выделенное превращалось в «ηελλο»). Добавлен WH_MOUSE_LL-хук
(только button-down, кликов мы не инжектим): любой клик = каретка/
выделение сместились → буфер и отложенный цикл/undo сбрасываются, и
следующий триггер корректно уходит в путь конверсии выделения. Тот же
резон, по которому за кликами следит macOS event tap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PZjjdW8bam8gQ872kmquy9
@MikeSviblov MikeSviblov changed the title fix(win): ANSI-маршалинг ToUnicodeEx (мусор вместо кириллицы + heap corruption) и Backspace в буфере fix(win): ANSI-маршалинг ToUnicodeEx, Backspace в буфере, клик мыши при выделении Oct 23, 2026
@MikeSviblov

Copy link
Copy Markdown
Author

Добавил третий фикс той же серии (живое тестирование беты): клик мыши не сбрасывал буфер клавиш. Сценарий: набрать текст, выделить его мышью, нажать триггер — триггер при непустом буфере уходит в путь «последнее слово», и его backspace'ы первым же нажатием съедают всё выделение, после чего поверх печатается конверсия последнего слова. Воспроизводится на main с любой парой раскладок.

Фикс: WH_MOUSE_LL-хук (только button-down; кликов приложение не инжектит, фильтрация маркером не нужна) — любой клик означает, что каретка/выделение сместились и буфер больше не отражает слово у каретки: буфер и отложенный reconvert сбрасываются, а следующий триггер корректно уходит в путь конверсии выделения. Это та же причина, по которой macOS event tap слушает клики.

Тесты: 30/30 на реальном Windows, вылетов и потери текста в сценарии «набрал → выделил мышью → триггер» больше нет (проверено на Win11 24H2).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant