- PVSM.RU - https://www.pvsm.ru -
[1]
Существует множество реализаций [2] компилятора С. Как такое может быть, что все они не в порядке, даже те, что были созданы одними из первых [3]? Библиотека времени выполнения Microsoft C определила то, как стандартная библиотека C должна работать в Windows, а все остальные реализации ради совместимости следовали за ней. Исключением я считаю платформу Cygwin [4] и её главный форк — MSYS2 [5], несмотря на то, что они не унаследовали описываемых недостатков. Они, в ходе эволюции, так сильно изменились, что, по сути, представляют собой совершенно новые платформы, которые нельзя назвать полностью соответствующими «обычной» Windows.
На практике стандартные библиотеки C++ реализованы на базе стандартной библиотеки C. Именно поэтому у C++ имеются те же проблемы, что и у C. CPython избегает этих проблем. Хотя эта реализация Python и написана на C, на Windows она обходит неправильную стандартную библиотеку C и напрямую обращается к проприетарным интерфейсам. Реализации других языков программирования, вроде gc в случае с Go, попросту создаются не на базе механизмов C, вместо этого, с самого начала, делая всё как нужно, поступая так, как библиотекам времени выполнения C стоило бы поступать уже давно.
Если вы работаете над единственным крупным проектом, обход библиотек времени выполнения C — это не так уж и сложно. И вы, вероятно, уже так и поступаете, обращаясь к важному функционалу платформы. Вам, по правде, даже и не нужна библиотека времени выполнения C. Но если вы заняты разработкой множества небольших программ (как я [6]), то написание особого кода для их поддержки в Windows быстро станет основной частью вашей работы. И, откровенно говоря, обеспечение поддержки Windows не стоит подобных усилий. Я пришёл к тому, что чаще всего просто принимаю то, что мне предлагают по умолчанию, хотя и знаю, что это приведёт к проблемам в Windows.
Прежде чем мы перейдём к деталям, хочу кое-что предложить тем, кто хочет легко и быстро решить вышеозначенные проблемы при работе с набором инструментов Mingw-w64, включая w64devkit [7]. Это — моя библиотека libwinsane [8]. Благодаря ей ваши консольные программы, написанные на C и C++, будут работать правильно. Она решает все проблемы, о которых идёт речь в этой статье, за исключением одной. При этом для применения этой библиотеки менять ваш исходный код не нужно. Достаточно просто связать её с вашей программой.
Существует две разновидности Windows API: «узкий», с суффиксом A (ANSI), и «широкий» (Unicode, UTF-16) с суффиксом W. Первый — это устаревший API, где активная кодовая страница отображает 256 байт на 256 конкретных символов (поддерживается до 256 символов). На типичных компьютерах, настроенных на работу с европейскими языками, это означает применение кодовой страницы Windows-1252 [9]. Грубо говоря [10], внутри Windows используется кодировка UTF-16, а вызовы, выполняемые через «узкий» интерфейс используют активную кодовую страницу для перевода «узких» строк в «широкие». В результате у обращений к «узкому» API есть лишь ограниченный доступ к системе.
Кодировка UTF-8 изобретена в 1992 году, она была стандартизирована в январе 1993 года. В последующие годы мир Unix принял эту кодировку из-за её обратной совместимости [11] с существующими интерфейсами. Программы могли читать и записывать Unicode-данные, могли пользоваться Unicode-путями, обрабатывать Unicode-аргументы, считывать значения Unicode-переменных окружения и устанавливать их значения. При этом в программах ничего не нужно было менять. В наши дни кодировка UTF-8 стала самым распространённым форматом кодирования текстовой информации. Во многом это так благодаря развитию Всемирной паутины.
В июле 1993 года Microsoft, с выходом Windows NT 3.1, представила «широкий» API Windows, сделав ставку на кодировку UCS-2 (позже — UTF-16), а не на UTF-8. Это, как оказалось, было ошибкой, так как UTF-16 практически во всём уступает [12] UTF-8. Правда, надо признать, что тогда некоторые проблемы не были особенно очевидными.
Главная проблема заключается в том, что стандартные библиотеки C и C++ подключены лишь к «узкому» интерфейсу Windows. Стандартная библиотека, а значит — и типичное переносимое приложение на Windows, не может обрабатывать ничего кроме ASCII-кода. Это приводит к тому, что эти программы не могут выполнять следующие действия в том случае, если они предусматривают применение символов, отличных от ASCII-символов:
Выполнение любого из этих действий требует вызова проприетарных функций. При этом Windows рассматривается в роли особой целевой платформы. Это часть того, что делает портирование программ на Windows весьма неприятным занятием. Разумное решение этой проблемы могло бы выглядеть как организация поддержки UTF-8 библиотекой времени выполнения C и подключение её к «широкому» API. Ещё один вариант решения проблемы заключается в том, что «узкий» API можно было бы перевести на UTF-8, постепенно отказываясь от концепции применения старой кодовой страницы. Это, в теории, то, что представляет собой «кодовая страница» UTF-8, хотя подобные решения оказываются работоспособными не всегда. При резком переходе на UTF-8 возникли бы проблемы с совместимостью. Но до совсем недавнего времени такая возможность не была представлена даже неким, условно говоря, «переключателем». Почему в Windows не может быть такого «переключателя» включив который можно обеспечить программам возможность нормально работать? Работать так же хорошо, как и на других платформах.
В 2019 году Microsoft представила возможность [13], позволяющую программам при запуске запрашивать UTF-8 в роли их активной кодовой страницы. Большее, чем раньше, количество функций «узкого» API получило поддержку UTF-8. Это похоже на тот «переключатель», о котором я мечтал, но мою радость несколько омрачает то, что для реализации этих возможностей надо особенным образом встраивать в бинарники некоторый объём неприглядного XML-кода. Но теперь у нас, по крайней мере, появилась некая стандартная возможность работать с UTF-8.
При использовании Mingw-64 это означает необходимость создания такого файла ресурсов:
#include <winuser.h>
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "utf8.xml"
Далее, компилируем это с помощью windres:
$ windres -o manifest.o manifest.rc
То, что получилось, связываем с программой. Это, удивительным образом, обычно работает! Программы могут получать доступ к Unicode-аргументам, могут работать с переменными окружения, с путями (в том числе — с помощью fopen). В общем, всё работает так же, как, уже десятилетия, работает на других платформах. Так как активная кодовая страница устанавливается в момент загрузки программы, это событие происходит до конструирования argv (из GetCommandLineA), и именно поэтому всё это и работоспособно.
В качестве альтернативы можно создать так называемую «параллельную сборку» (SxS, side-by-side assembly), поместив этот XML-код в файл с тем же именем, что и у EXE-файла, но с расширением .manifest (после расширения .exe), а после этого положив этот файл около EXE-файла. Пользуясь этим приёмом, стоит помнить о существовании SxS-кеша (WinSxS). Изменения в соответствующих файлах могут быть видны лишь через некоторое время после их выполнения.
При использовании описываемого метода, правда, не работает консольный ввод/вывод. Консоль является, по отношению к процессу, внешней сущностью, поэтому на неё требования процесса к активной кодовой странице не распространяются. Её нужно особо настраивать, пользуясь проприетарным вызовом:
SetConsoleOutputCP(CP_UTF8);
Это, конечно, так себе занятие, но, по крайней мере, теперь всё не так плохо, как раньше. Правда, эта настройка имеет отношение лишь к выводу данных. То есть — программы могут писать в консоль, пользуясь UTF-8, но не читать из консоли. К сожалению, возможность чтения UTF-8-текстов из консоли всё ещё не работает [14]. При установке кодовой страницы для входных данных нам сообщают об успешном проведении операции, но этим всё и ограничивается.
SetConsoleCP(CP_UTF8); // не работает
Если вам нужно читать Unicode-данные из консоли в интерактивном режиме — вам не остаётся ничего кроме обхода библиотеки [15] времени выполнения C, так как вышеописанный механизм всё ещё неработоспособен.
Ещё одна давняя проблема программирования для Windows на C и C++ заключается в наличии отличающихся друг от друга «текстовых» и «двоичных» потоков, унаследованных от DOS. В основном это означает автоматическое преобразование символов перевода строки (CRLF и LF). Стандарт C это недвусмысленно позволяет, но на Unix-подобных платформах никогда не делалось различия между текстовыми и двоичными потоками.
Стандарт, кроме того, указывает на то, что стандартные потоки ввода, вывода и ошибок открываются в виде текстовых потоков, при этом нет переносимого способа переключить поток в двоичный режим. Это серьёзный недостаток стандарта. В Unix-подобных системах это неважно, но в Windows это означает, что программа не может читать или писать двоичные данные при работе со стандартными потоками, не вызывая нестандартные функции. Это означает ещё и то, что чтение стандартных потоков и запись в них выполняются медленно. Часто это становится узким местом [16] программ, если только создатель программы не обойдёт этот недостаток.
Лично я предпочитаю писать двоичные данные [17] в стандартный поток вывода, в том числе — видеоданные [18], и иногда пользуюсь двоичными фильтрами [19], которые тоже читают двоичные входные данные. Я делаю это так часто, что, вероятно, в половине моих C-программ, в функции main, имеется такой фрагмент кода, обеспечивающий их правильную работу в Windows:
#ifdef _WIN32
int _setmode(int, int);
_setmode(0, 0x8000);
_setmode(1, 0x8000);
#endif
Эта магическая формула устанавливает стандартные потоки ввода и вывода в библиотеке времени выполнения C в двоичный режим. При этом не нужны никакие заголовочные файлы, код получается компактным, простым и самодостаточным.
Вышеописанное встроенное преобразование символов перевода строки, а также стандартный текстовый редактор Windows, Notepad, сильно отстающий [20] от других систем, стали причиной того, что многие другие программы, включая Git, создали собственные, вызывающие неудобства, «полезные» возможности [21] по преобразованию символов перевода строки, которые привели к другим проблемам [22].
В начале статьи я говорил о моей библиотеке libwinsane. Она позволяет исправить все вышеописанные проблемы путём простого связывания её с программой. Сюда входит использование раздела .rsrc XML-манифеста, настройка консоли на вывод UTF-8-текстов, перевод стандартных потоков в двоичный режим. Всё это выполняется до вызова функции main (с помощью конструктора GCC). Я называю мою разработку «библиотекой», но это, на самом деле, всего лишь объектный файл. Эта разработка не может быть представлена в виде статической библиотеки, так как она должна быть связана с программой, несмотря на то, что в коде программы она нигде не упоминается.
Вот программа:
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char *arg = argv[argc-1];
size_t len = strlen(arg);
printf("%zu %sn", len, arg);
}
В обычных условиях её компилируют и запускают так:
C:>cc -o example example.c
C:>example π
1 p
Как всегда, Unicode-аргументы по-тихому ужимаются до одного байта. А теперь свяжем эту программу с libwinsane:
C:>gcc -o example example.c libwinsane.o
C:>example π
2 π
Это приведёт к тому, что в Windows программа заработает так же, как на любой другой платформе.
Если вы занимаетесь поддержкой достаточно крупной программы, то вы, возможно, решите внедрить в свой проект необходимые вам части libwinsane, вместо того, чтобы постоянно связывать его с представленным объектным файлом, который даже не используется в самой программе. Причины его существования заключаются в основном в удобстве использования и в том, чтобы сжато продемонстрировать мои идеи. А в своей версии моего кода вы можете даже решить организовать обработку управляющих последовательностей ANSI [23].
С какими проблемами вы сталкивались, программируя для Windows на C и C++?
Автор:
ru_vds
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/371458
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/ruvds/blog/645325/
[2] множество реализаций: https://nullprogram.com/blog/2016/06/13/
[3] одними из первых: https://nullprogram.com/blog/2018/04/13/
[4] Cygwin: https://www.cygwin.com/
[5] MSYS2: https://www.msys2.org/
[6] как я: https://github.com/skeeto/scratch
[7] включая w64devkit: https://nullprogram.com/blog/2020/05/15/
[8] libwinsane: https://github.com/skeeto/scratch/tree/master/libwinsane
[9] кодовой страницы Windows-1252: https://en.wikipedia.org/wiki/Windows-1252
[10] Грубо говоря: http://simonsapin.github.io/wtf-8/
[11] обратной совместимости: https://nullprogram.com/blog/2017/10/06/#what-is-utf-8
[12] уступает: http://utf8everywhere.org/
[13] возможность: https://docs.microsoft.com/en-us/windows/apps/design/globalizing/use-utf8-code-page
[14] не работает: https://github.com/microsoft/terminal/issues/4551#issuecomment-585487802
[15] обхода библиотеки: https://nullprogram.com/blog/2020/05/04/
[16] узким местом: https://nullprogram.com/blog/2021/12/04/
[17] писать двоичные данные: https://nullprogram.com/blog/2020/06/29/
[18] видеоданные: https://nullprogram.com/blog/2020/11/24/
[19] двоичными фильтрами: https://nullprogram.com/blog/2017/07/02/
[20] отстающий: https://devblogs.microsoft.com/commandline/extended-eol-in-notepad/
[21] «полезные» возможности: https://github.com/skeeto/w64devkit/issues/10
[22] другим проблемам: https://github.com/skeeto/binitools/commit/2efd690c3983856c9633b0be66d57483491d1e10
[23] управляющих последовательностей ANSI: https://github.com/skeeto/hastyhex/blob/master/hastyhex.c#L220
[24] Источник: https://habr.com/ru/post/645325/?utm_source=habrahabr&utm_medium=rss&utm_campaign=645325
Нажмите здесь для печати.