Поддержка Юникода в Windows появилась раньше, чем в большинстве остальных операционных систем. Из-за этого многие проблемы, связанные с представлением символов, в Windows решались не так, как в других системах, разработчики которых отложили внедрение нового стандарта до лучших времён [1]. Самый показательный пример: в Windows для представления символов Юникода используется кодировка UCS-2. Она была рекомендована Консорциумом Юникода, поскольку версия 1.0 поддерживала только 65 536 символов [2]. Пять лет спустя Консорциум передумал, но к тому времени менять что-то в Windows было уже поздно, так как на рынок уже были выпущены системы Win32s, Windows NT 3.1, Windows NT 3.5, Windows NT 3.51 и Windows 95 — все они использовали кодировку UCS-2 [3].

Но сегодня мы поговорим о строках форматирования функции printf.

Поскольку Юникод был принят в Windows раньше, чем в языке C, это означало, что разработчики Microsoft должны были придумать, как реализовать поддержку этого стандарта в среде выполнения C. В результате появились такие функции, как wcscmp, wcschr и wprintf. Что же касается строк форматирования в printf, то для них ввели следующие спецификаторы:

  • %s представляет строку той же ширины, что и строка форматирования;
  • %S представляет строку с шириной, обратной ширине строки форматирования;
  • %hs представляет обычную строку независимо от ширины строки форматирования;
  • %ws и %ls представляют широкую строку независимо от ширины строки форматирования.

Идея состояла в том, чтобы можно было написать такой код:

TCHAR buffer[256];
GetSomeString(buffer, 256);
_tprintf(TEXT("The string is %s.\n"), buffer);

И при компиляции в режиме ANSI получить вот такой результат:

char buffer[256];
GetSomeStringA(buffer, 256);
printf("The string is %s.\n", buffer);

А при компиляции в режиме Юникод — такой [4]:

wchar_t buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);

Поскольку спецификатор %s принимает строку той же ширины, что у строки форматирования, такой код будет работать корректно и в формате ANSI, и в формате Юникод. Также это решение очень упрощает преобразование уже написанного кода из формата ANSI в формат Юникод, так как на место спецификатора %s подставляется строка нужной ширины.

Когда поддержка Юникода была официально добавлена в C99, комитет по стандартизации языка C принял другую модель строк форматирования для функции printf:

  • %s и %hs представляют обычную строку;
  • %ls представляет широкую строку.

Тут-то и начались проблемы. За прошедшие к тому моменту шесть лет для Windows было написано огромное множество программ объёмом в миллиарды строк, и в них использовался старый формат. Как быть компиляторам Visual C и C++?

Было решено остаться на старой, нестандартной модели, чтобы не сломать все существующие в мире программы под Windows.

Если вы хотите, чтобы ваш код работал и в тех средах исполнения, которые придерживаются классических правил для printf, и в тех, которые следуют правилам стандарта C, вам придётся ограничиться спецификаторами %hs для обычных строк и %ls для широких. В этом случае гарантируется постоянство результатов, независимо от того, передаётся строка форматирования в функцию sprintf или wsprintf.

#ifdef UNICODE
#define TSTRINGWIDTH TEXT("l")
#else
#define TSTRINGWIDTH TEXT("h")
#endif
TCHAR buffer[256];
GetSomeString(buffer, 256);
_tprintf(TEXT("The string is %") TSTRINGWIDTH TEXT("s\n"), buffer);
char buffer[256];
GetSomeStringA(buffer, 256);
printf("The string is %hs\n", buffer);
wchar_t buffer[256];
GetSomeStringW(buffer, 256);
wprintf("The string is %ls\n", buffer);

Вынесенное отдельно определение TSTRINGWIDTH позволяет писать, например, вот такой код:

_tprintf(TEXT("The string is %10") TSTRINGWIDTH TEXT("s\n"), buffer);

Поскольку людям нравится табличное представление информации, вот вам таблица.


Я выделил строки со спецификаторами, которые в C определены так же, как и в классическом формате, принятом в Windows [5]. Используйте эти спецификаторы, если хотите, чтобы ваш код выдавал одинаковые результаты в обоих форматах.

Примечания

[1] Казалось бы, внедрение Юникода в Windows раньше прочих систем должно было дать Microsoft преимущество первого хода, но — по крайней мере в случае с Юникодом — оно обернулось для них «проклятием первопроходца», потому что остальные решили просто подождать до лучших времён, когда появятся более перспективные решения (такие как кодировка UTF-8), и только после этого внедрять Юникод в свои системы.

[2] Видимо, они полагали, что 65 536 символов должно было хватить на всех.

[3] Позже её заменили на UTF-16. К счастью, UTF-16 имеет обратную совместимость с UCS-2 для тех кодовых знаков, которые могут быть представлены в обеих кодировках.

[4] Формально версия для Юникода должна выглядеть так:

unsigned short buffer[256];
GetSomeStringW(buffer, 256);
wprintf(L"The string is %s.\n", buffer);

Дело в том, что wchar_t тогда ещё не был самостоятельным типом, и пока его не добавили в стандарт, он был всего лишь синонимом unsigned short. О перипетиях судьбы wchar_t можно почитать в отдельной статье.

[5] Классический формат, разработанный Windows, появился первым, так что это скорее стандарту C пришлось подстраиваться под него, а не наоборот.

Примечание переводчика

Я благодарен автору за эту публикацию. Теперь стало понятно, как получилась вся эта путаница с "%s". Дело в то том, что наши пользователи постоянно задавали вопрос, почему PVS-Studio по-разному реагирует на их, как им кажется, «переносимый» код, в зависимости собирают они свой проект под Linux или Windows. Понадобилось сделать в описании диагностики V576 специальный отдельный раздел, посвященный этой теме (см. «Широкие строки»). После этой статьи всё становится ещё более понятно и очевидно. Думаю, эту заметку стоит прочитать всем, кто разрабатывает кроссплатформенные приложения. Читайте и расскажите коллегам.

Комментарии (17)


  1. Tantrido
    10.09.2019 14:30

    Поддержка Юникода в Windows появилась раньше, чем в большинстве остальных операционных систем.
    Вызывающе неверная информация: раньше всего появилась в Linux-е (сразу всё в комплексе локаль, приложения, библиотеки и ФС и т.д.), а в Windows гораздо позже, неполноценно и криво (на ФС не было).


    1. red_andr
      10.09.2019 18:46
      +2

      Прям уж «вызывающе неверная»! На самом деле «появилась раньше, чем в большинстве» никак не противоречит «раньше всего появилась в Линуксе». «Большинство» же не значит «все остальные». И да, когда конкретно поддержка Юникода в Линуксе появилась?


    1. apro
      10.09.2019 19:43

      Вызывающе неверная информация: раньше всего появилась в Linux

      Если имеется ввиду использование utf-8 в Linux то это теоретически невозможно.
      utf-8 в том виде который мы ее знаем была разработана в рамках работы над ОС Plan-9,
      поэтому естественно первая ОС с utf-8 это Plan-9 и в Linux utf-8 могла появиться только позже Plan-9.


    1. Sabubu
      10.09.2019 21:38
      -1

      В линуксе поддержка Юникода оставала от Windows. utf-8 они использовали, так как было огромное количество программ, рассчитанных на 8-битные кодировки.


      1. Tantrido
        10.09.2019 21:44

        В чём отставала?! В линуксе давно на UTF-8 сидел, когда в винде (параллельно стояла) ещё и в помине не было, может возможность была, но в приложениях, ни в локали, ни ФС (и сейчас кажись нету) не было.


        1. VEG
          10.09.2019 23:23
          +1

          Мир не ограничивался Windows 95/98, которые были временным решением для домашних пользователей, пока линейка NT созревала для этих целей.

          Windows NT 3.1 вышла в 1993, и она уже поддерживала Unicode. NTFS появилась тогда же, и она изначально поддерживает исключительно Unicode.


          1. Tantrido
            10.09.2019 23:39

            Удивительно! :) Значит я пропустил: я сидел на пользовательских 3.1/95/98/XP… и там с кодировками была ж…


            1. jaiprakash
              11.09.2019 10:51

              2000 и XP из ветки NT, и там можно было использовать NTFS.


  1. amarao
    10.09.2019 14:35
    +1

    Windows 95 — все они использовали кодировку UCS-2 — Windows 95 не использовала кодировку UCS-2, т.к. у неё не было никаких w-функций, только A функции (CreateWindowsW VS CreateWindowsA).


    1. KanuTaH
      10.09.2019 15:33

      Ну тащем-та это не совсем так, для 95/98/ME была такая штука под названием Microsoft Layer for Unicode (MSLU), в ней были w-версии всех этих функций.


    1. Aloraman
      10.09.2019 17:53

      Если совсем строго, то еще в Win 3.x вместе с родным Win16 API было еще некоторое подмножество Win32 (Win32S = Win32 Subset), в котором A-функции шли вместе с W-функциями, только вместо имплементаций у W-функций там были заглушки, возвращающие ошибки.
      95/98/ME хоть и имели формально полную Win32-подсистему, но большинство W-функций там тоже шло заглушками. Большинство, но не все. Некоторые функции (их довольно мало) для работы с ресурсами (e.g. EnumResourceNamesW), командной строкой (e.g. GetCommandLineW), диалоговыми сообщениями (e.g. MessageBoxW) имели рабочую реализацию, также были доступны вспомогательные функции для конвертации W-строк в A-строки (MultiByteToWideChar, WideCharToMultiByte) с кодированием MBCS (Multibyte Character Set). В 98 добавили еще чуток функций (lstrlenW, lstrcpyW, lstrcatW). Полноценная поддержка Win32 через MSLU (так же известная как unicows) для них появилась только в 2001 году.


  1. slonopotamus
    10.09.2019 15:25
    +1

    Unicode != wchar_t.


  1. Alexey_Alive
    10.09.2019 16:42
    -1

    Зачем, вообще, использовать указатель на wchar? char вполне нормально работает и для UTF-8, и для UTF-32. А гарантировать, что один видимый символ = один символ юникода даже UTF-32 не может.


    1. KanuTaH
      10.09.2019 17:17

      Так-то оно так, но… насколько я понимаю, изначально wchar_t вводился для того, чтобы работали штуки типа str[i]. Это не юникод, и вообще не какой-то стандарт, а просто некое такое компиляторозависимое представление символов, что все символы имеют фиксированную длину в байтах.


  1. ip1981
    10.09.2019 17:53

    За прошедшие к тому моменту шесть лет для Windows было написано огромное множество программ объёмом в миллиарды строк.

    Ого! Миллиарды :)


  1. qw1
    10.09.2019 20:55

    Пора уже забыть про TCHAR и, соответственно, _tprintf, и, хуже того, _tmain.


  1. agmt
    11.09.2019 08:39

    Благо, в 2019 наконец можно использовать нормальные char в Windows: docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page