- PVSM.RU - https://www.pvsm.ru -

Капля здравого смысла для Windows-разработки на C и C++

Суровая действительность разработки на C и C++ для Windows такова: для этой платформы никогда не существовало качественной, нативной реализации стандартной библиотеки этих языков. Стандартная библиотека должна абстрагировать механизмы базовой системы ради упрощения разработки переносимого программного обеспечения. С и C++ на Windows очень плохо состыкованы с интерфейсами операционной системы. В результате большая часть переносимых, или, так сказать, «почти всегда переносимых» программ, которые отлично работают практически везде, в Windows оказываются едва заметно «поломанными», в особенности — за пределами англоговорящего мира. Причины этого почти наверняка связаны с политикой тех или иных компаний, с искусственными ограничениями, а не с техническими особенностями систем, что лишь усугубляет положение. Эта статья посвящена рассказу о проблемах Windows-разработки на C и C++ и о том, как они выражаются. Здесь же будут представлены некоторые простые методы борьбы с этими проблемами при разработке переносимого ПО.

Капля здравого смысла для Windows-разработки на C и C++ - 1 [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 не может быть такого «переключателя» включив который можно обеспечить программам возможность нормально работать? Работать так же хорошо, как и на других платформах.

Как решить почти все проблемы поддержки Unicode?

В 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

В начале статьи я говорил о моей библиотеке 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