СЕНТЯБРЬ 2016
ТОМ 31, НОМЕР 9
C++ - Преобразования Unicode-кодировок с помощью STL-строк и Win32 API
Джованни Диканио | СЕНТЯБРЬ 2016
Исходный код можно скачать по ссылке
Продукты и технологии:
C++ Unicode, Win32 API, STL-строки
В статье рассматриваются:
- кодировки UTF-8 и UTF-16;
- преобразование между UTF-8 и UTF-16 через Win32 API;
- обертывание MultiByteToWideChar в C++-функцию в современном стиле для преобразований UTF-8 в UTF-16;
- обработка ошибок с помощью C++-исключений.
Unicode является стандартом де-факто для представления текста на любых языках в современном программном обеспечении. Согласно веб-сайту официального консорциума Unicode (bit.ly/1Rtdulx), “Unicode предоставляет уникальное число для каждого символа независимо от платформы, программы и языка.” Каждое из этих уникальных чисел называют кодовой точкой (code point), и обычно она представляется с использованием префикса “U+”, за которым следует уникальное число в шестнадцатеричной форме. Так, кодовая точка, связанная с символом “C” — U+0043. Заметьте, что Unicode является отраслевым стандартом, охватывающим большинство систем письма в мире, включая иероглифы. Например, японская кана (ru.wikipedia.org/wiki/%D0%9A%D0%B0%D0%BD%D0%B0) содержит иероглиф 学 (среди других значений обозначающий обучение и знание), который связан с кодовой точкой U+5B66. В настоящее время в стандарте Unicode определены более 1 114 000 кодовых точек.
От абстрактных кодовых точек к реальным битам: кодировки UTF-8 и UTF-16
Кодовая точка — это абстрактная концепция. Для программиста вопрос заключается в том, как эти кодовые точки Unicode представляются в конкретном виде, используя биты? Ответ на этот вопрос ведет прямо к концепции Unicode-кодировки. По сути, Unicode-кодировка — конкретный, четко определенный способ представления значений кодовых точек Unicode в битах. В стандарте Unicode определено несколько кодировок, но наиболее важные из них — UTF-8 и UTF-16; обе они являются кодировками переменной длины (variable-length encodings), способными кодировать все возможные символы Unicode или, точнее, кодовые точки. Поэтому преобразования между этими двумя кодировками осуществляются без потерь: ни один символ Unicode не будет потерян в ходе этого процесса.
В UTF-8, как и предполагает название этой кодировки, используются восьмибитовые кодовые единицы (code units). Она была разработана с учетом двух важных характеристик. Во-первых, она обратно совместима с ASCII; это означает, что каждый допустимый ASCII-код символа имеет то же байтовое значение, что и при кодировании в UTF-8. Иначе говоря, допустимый ASCII-текст автоматически является допустимым текстом в кодировке UTF-8.
Во-вторых, поскольку Unicode-текст, закодированный в UTF-8, — это просто последовательность восьмибитовых байтов, исчезает проблема с порядком следования байтов (endianness). Кодировка UTF-8 (в отличие от UTF-16) по своей природе нейтральна к порядку следования байтов. Это важная особенность при обмене текстом между разными вычислительными системами, которые могут иметь разные аппаратные архитектуры с разным порядком следования байтов.
Если вспомнить о ранее упомянутых двух Unicode-символах, то заглавная буква “C” (кодовая точка U+0043) кодируется в UTF-8 одним байтом 0x43 (значение 43 шестнадцатеричное), который точно соответствует ASCII-коду, связанному с этим символом (поскольку UTF-8 обратно совместима с ASCII). Напротив, японский иероглиф 学 (кодовая точка U+5B66) кодируется в UTF-8 как последовательность из трех байтов: 0xE5 0xAD 0xA6.
UTF-8 — самая популярная Unicode-кодировка в Интернете. Согласно последним статистическим сведениям W3Techs, доступным по ссылке bit.ly/1UT5EBC, UTF-8 используется на 87% всех проанализированных веб-сайтов.
UTF-16 фактически является стандартом де-факто кодировки, используемой Windows API-функциями с поддержкой Unicode. UTF-16 — “родная” Unicode-кодировка и во многих других программных системах. Так, Qt, Java, библиотека International Components for Unicode (ICU) и прочие используют кодировку UTF-16 для хранения Unicode-строк.
В UTF-16 применяются 16-битные кодовые единицы. Как и UTF-8, UTF-16 позволяет кодировать все возможные в Unicode кодовые точки. Однако, если UTF-8 кодирует каждую допустимую в Unicode кодовую точку, используя от одного до четырех восьмибитовых байтов, UTF-16 в каком-то смысле проще. По сути, кодовые точки Unicode кодируются в UTF-16 всего одним или двумя 16-битными кодовыми единицами. Но наличие кодовых единиц размером больше одного байта влечет за собой проблемы с порядком следования байтов: фактически существует и “остроконечная” (big-endian) (от младшего байта к старшему) UTF-16, и “тупоконечная” (little-endian) (от старшего байта к младшему) UTF-16. Тогда как кодировка UTF-8 только одна, поскольку нейтральна к порядку следования байтов.
В Unicode определена концепция плоскости (plane) как непрерывной группы 65 536 (216) кодовых точек. Первая плоскость идентифицируется как плоскость 0, или Basic Multilingual Plane (BMP). Символы почти для всех современных языков и многие знаки находятся в BMP, а все эти BMP-символы представляются в UTF-16 одной 16-битной кодовой единицей.
Дополнительные символы (supplementary characters) расположены в плоскостях, отличных от BMP; они включают пиктографические символы вроде Emoji и символы вышедших из употребления письменностей наподобие египетских иероглифов. Эти дополнительные символы вне BMP кодируются в UTF-16 двумя 16-битными кодовыми единицами, также известными как суррогатные пары (surrogate pairs).
Заглавная буква “C” (U+0043) кодируется в UTF-16 как одна 16-битная кодовая единица 0x0043, а иероглиф 学 (U+5B66) — как одна 16-битная кодовая единица 0x5B66. Для многих Unicode-символов существует прямое соответствие между их абстрактным представлением в виде кодовой точки (скажем, U+5B66) и их шестнадцатеричной кодировкой в UTF-16 (например, 16-битное слово 0x5B66).
Чтобы слегка поразвлечься, давайте посмотрим на некоторые пиктографические символы. Unicode-символ “снеговик” (, U+2603) кодируется в UTF-8 как трехбайтовая последовательность: 0xE2 0x98 0x83; однако его эквивалент в UTF-16 является одной 16-битной кодовой единицей 0x2603. Unicode-символ “кружка пива” (, U+1F37A), находящийся вне BMP, кодируется в UTF-8 как четырехбайтовая последовательность: 0xF0 0x9F 0x8D 0xBA. Его эквивалент в UTF-16 использует две 16-битные кодовые единицы, 0xD83C 0xDF7A, которые являются примером суррогатной пары в UTF-16.
Преобразования между UTF-8 и UTF-16 через Win32 API
Как обсуждалось выше, Unicode-текст представляется в памяти компьютера с использованием разных битов в зависимости от конкретной Unicode-кодировки. Какую кодировку вы должны использовать? Однозначного ответа на этот вопрос нет.
Сравнительно недавно стало общепринятым хранить Unicode-текст, закодированный в UTF-8, в экземплярах класса std::string в кросс-платформенном C++-коде. Более того, существует принципиальное соглашение о том, что UTF-8 является лучшей кодировкой для обмена текстом через границы приложений и разные аппаратные платформы. Тот факт, что UTF-8 нейтральна к порядку следования байтов, сыграл важную роль в этом. В любом случае преобразования между UTF-8 и UTF-16 необходимы по крайней мере на границе Win32 API, поскольку Windows-функции с поддержкой Unicode использую UTF-16 в качестве “родной” кодировки.
Теперь углубимся в код на C++, который реализует эти преобразования между Unicode-кодировками UTF-8 и UTF-16. Для этой цели можно использовать две ключевые Win32-функции: MultiByteToWideChar и симметричную ей WideCharToMultiByte. Первую вызывают для преобразования из UTF-8 (“многобайтовой” строки в специфической терминологии Win32 API) в UTF-16 (“широкобайтовую” строку), а вторую — для обратной задачи. Поскольку эти Win32-функции имеют сходные интерфейсы и шаблоны использования, в этой статье я сосредоточусь только на MultiByteToWideChar, но в сопутствующий компилируемый C++-код включена и другая функция.
Применение стандартных строковых STL-классов для хранения Unicode-текста Поскольку это статья по C++, справедливо ожидать хранения Unicode-текста в одном из строковых классов. Теперь возникает вопрос: какой вид строковых C++-классов можно использовать для хранения Unicode-текста? Ответ зависит от конкретной кодировки Unicode-текста. Если применяется кодировка UTF-8, то, поскольку она основана на восьмибитовых кодовых единицах, для представления каждой из этих единиц в C++ годится простой char. В этом случае STL-класс std::string, основанный на char, — хороший вариант для хранения Unicode-текста в кодировке UTF-8.
С другой стороны, если Unicode-текст закодирован в UTF-16, каждая кодовая единица представляется 16-битными словами. В Visual C++ тип wchar_t имеет как раз размер в 16 бит; соответственно STL-класс std::wstring, основанный на wchar_t, отлично подходит для хранения Unicode-текста в кодировке UTF-16.
Стоит отметить, что стандарт C++ не определяет размер типа wchar_t, поэтому, хотя компилятор Visual C++ использует для него 16 битов, другие компиляторы C++ могут оперировать другими размерами. И фактически размер wchar_t, определяемый компилятором GNU GCC C++ в Linux, составляет 32 бита. Так как тип wchar_t имеет разные размеры в разных компиляторах и платформах, класс std::wstring, основанный на этом типе, не является портируемым. Иначе говоря, wstring можно использовать для хранения Unicode-текста, закодированного в UTF-16, в Windows с компилятором Visual C++ (где размер wchar_t равен 16 битам), но не в Linux с компилятором GCC C++, который определяет 32-битный тип wchar_t.
На самом деле существует еще одна Unicode-кодировка, менее известная и реже используемая на практике, чем ее родственники: UTF-32. Как и предполагает ее название, она основана на 32-битных кодовых единицах. Поэтому 32-битный wchar_t из GCC/Linux — хороший кандидат для кодировки UTF-32 на платформе Linux.
Этой неоднозначностью размера wchar_t определяется отсутствие портируемости C++-кода, оперирующего с этим типом (включая сам класс std::wstring). С другой стороны, std::string, основанный на char, является портируемым. Но с практической точки зрения, следует отметить, что применение wstring для хранения текста в кодировке UTF-16 прекрасно подходит в C++-коде, специфичном для Windows. По сути, эти части кода уже взаимодействуют с Win32 API, которые специфичны для платформы просто по определению. Поэтому добавление wstring в смесь никак не меняет ситуацию.
Наконец, важно отметить, что из-за варьируемой длины в кодировках UTF-8 и UTF-16 возвращаемые значения методов string::length и wstring::length, как правило, не соответствуют количеству Unicode-символов (или кодовых точек), хранимых в строках.
Интерфейс функции преобразования Давайте разработаем функцию для преобразования Unicode-текст, закодированного в UTF-8, в эквивалентный текст с кодировкой UTF-16. Она может оказаться удобной, например когда у вас есть какой-то кросс-платформенный C++-код, который хранит Unicode-строки в кодировке UTF-8, используя STL-класс std::string, и вы хотите передавать этот текст в Win32-функции с поддержкой Unicode, обычно использующие кодировку UTF-16. Поскольку этот код взаимодействует с Win32 API, он уже не является портируемым, так что в данном случае прекрасно подходит std::wstring. Возможный прототип функции выглядит так:
std::wstring Utf8ToUtf16(const std::string& utf8);
Эта функция преобразования принимает в качестве ввода Unicode-строку в кодировке UTF-8, которая хранится в стандартном STL-классе std::string. Поскольку это входной параметр, он передается в функцию по const-ссылке (const &). После преобразования возвращается строка, закодированная в UTF-16, хранящаяся в экземпляре std::wstring. Однако при конвертации Unicode-кодировок вполне возможны проблемы. Например, входная строка UTF-8 может содержать недопустимую для UTF-8 последовательность (что может быть результатом ошибки в других частях кода или неких злонамеренных действий). В таких случаях самое лучшее с точки зрения безопасности — прекращать преобразование как неудавшееся вместо использования потенциально опасной последовательности байтов. Функция преобразования может обрабатывать случаи с недопустимыми входными последовательностями UTF-8, генерируя C++-исключение.
Определение класса исключения для ошибок преобразования Какого рода C++-класс можно применить для генерации исключения в случае неудачного преобразования Unicode-кодировки? Как вариант можно было бы использовать класс, уже определенный в стандартной библиотеке, например std::runtime_error. Однако я предпочитаю определить новый пользовательский C++-класс исключения для этой цели, унаследовав его от std::runtime_error. Когда Win32-функции вроде MultiByteToWideChar выдают ошибку, есть возможность вызывать GetLastError, чтобы получить подробную информацию о причине неудачи. Так, в случае недопустимых последовательностей UTF-8 во входной строке типичный код ошибки, возвращаемый GetLastErorr, — ERROR_NO_UNICODE_TRANSLATION. Имеет смысл добавить эту часть информации в пользовательский C++-класс исключения; она может оказаться полезной при отладке. Определение этого класса исключения начинается с:
// utf8except.h
#pragma once
#include <stdint.h> // для uint32_t
#include <stdexcept> // для std::runtime_error
// Представляет ошибку при преобразованиях кодировки UTF-8
class Utf8ConversionException
: public std::runtime_error
{
// Код ошибки от GetLastError()
uint32_t _errorCode;
Заметьте, что значение, возвращаемое GetLastError, имеет тип DWORD, который представляет 32-битное целое без знака. Однако DWORD является не портируемым typedef, специфичным для Win32. Но, даже если этот C++-класс исключения генерируется из Win32-специфичных частей C++-кода, он может быть захвачен кросс-платформенным C++-кодом! Поэтому имеет смысл использовать портируемые typedef вместо специфичных для Win32; uint32_t — пример такого типа.
Затем можно определить конструктор, который инициализирует экземпляры этого пользовательского класса исключения сообщением об ошибке и ее кодом:
public:
Utf8ConversionException(
const char* message,
uint32_t errorCode
)
: std::runtime_error(message)
, _errorCode(errorCode)
{ }
Наконец, можно определить открытый аксессор get, обеспечивающий доступ к коду ошибки только для чтения:
uint32_t ErrorCode() const
{
return _errorCode;
}
}; // класс исключения
Поскольку этот класс наследует от std::runtime_error, можно вызвать метод what, чтобы получить сообщение об ошибке, переданное в конструктор. Заметьте, что в определении этого класса используются лишь портируемые стандартные элементы, так что этот класс отлично работает в кросс-платформенных частях C++-кода, даже находящихся далеко от специфичной для Windows точки генерации.
Преобразование из UTF-8 в UTF-16: MultiByteToWideChar в действии
Теперь, когда определен прототип функции преобразования и реализован пользовательский C++-класс исключения для корректного представления ошибок преобразования UTF-8, пора создавать тело этой функции. Как и ожидалось, преобразование из UTF-8 в UTF-16 можно выполнить с помощью Win32-функции MultiByteToWideChar. Термины “многобайтовый” и “широкосимвольный” имеют исторические корни. Изначально эта API-функция и симметричная ей WideCharToMultiByte предназначались в основном для преобразования в Win32-функциях с поддержкой Unicode между текстом, хранящемся в специфических кодовых страницах, и Unicode-текстом, который использует кодировку UTF-16. Широкий символ относится к wchar_t, поэтому он связан со строкой на основе wchar_t, которая является строкой в кодировке UTF-16. Многобайтовая строка, напротив, — это последовательность байтов, выраженная в кодовой странице. Устаревшая концепция кодовой страницы была потом расширена для включения кодировки UTF-8.
Типичный шаблон использования этой API-функции заключается в том, что сначала вы вызываете MultiByteToWideChar, чтобы получить размер конечной строки. Затем создаете некий строковый буфер с этим размером. Обычно это делается вызовом метода std::wstring::resize в случае, если в нем будет храниться строка в кодировке UTF-16. (Подробнее об этом см. мою статью “Using STL Strings at Win32 API Boundaries” за июль 2015 года по ссылке msdn.com/magazine/mt238407.) Наконец, функция MultiByteToWideChar вызывается во второй раз для выполнения преобразования кодировки, используя ранее созданный строковый буфер. Заметьте, что тот же шаблон применяется и к симметричной API-функции WideCharToMultiByte.
Давайте реализуем этот шаблон в коде на C++, в теле пользовательской функции Utf8ToUtf16. Начнем с обработки особого случая с пустой входной строкой, где возвращается пустая выходная строка типа wstring:
#include <Windows.h> // для Win32-функций
#include <string> // для std::string и std::wstring
std::wstring Utf8ToUtf16(const std::string& utf8)
{
std::wstring utf16; // результат
if (utf8.empty())
{
return utf16;
}
Флаги преобразования MultiByteToWideChar можно вызвать в первый раз для получения размера конечной строки в UTF-16. Эта Win32-функция имеет сравнительно сложный интерфейс, и ее поведение определяется согласно флагам. Поскольку эта API-функция будет вызываться в теле функции преобразования Utf8ToUtf16 дважды, хорошая практика для надежности кода и удобства его сопровождения — определять именованную константу, которая может использоваться в обоих вызовах:
// Надежный провал, если во входной строке обнаружен
// недопустимый символ UTF-8
constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;
Безусловный провал преобразования, если во входной строке обнаруживается недопустимая для UTF-8 последовательность, является хорошей практикой и с точки зрения безопасности. Применение флага MB_ERR_INVALID_CHARS также приветствуется в книге Майкла Говарда (Michael Howard) и Дэвида Лебланка (David LeBlanc) “Writing Secure Code, Second Edition” (Microsoft Press, 2003).
Если ваш проект использует более старую версию компилятора Visual C++, которая не поддерживает ключевое слово constexpr, вы можете заменить его на static const в этом контексте.
Длины строк и безопасные преобразования из size_t в int MultiByteToWideChar ожидает, что параметр с длиной входной строки выражен типом int, тогда как метод length в строковых STL-классах возвращает значение типа, эквивалентного size_t. В 64-разрядных сборках компилятор Visual C++ генерирует предупреждение, указывающее на потенциальную опасность потери данных при преобразовании из size_t (размер 8 байтов) в int (размер 4 байта). Но даже в 32-разрядных сборках, где size_t и int определяются компилятором Visual C++ как 32-битные целые, имеется несовпадению “без знака/со знаком”: size_t — беззнаковый тип, а int — со знаком. Это не проблема для строк разумной длины, но для гигантских строк с размером более 231–1, т. е. длиной свыше двух миллиардов байтов, преобразование из целого без знака (size_t) в целое со знаком (int) может генерировать отрицательное число, а отрицательные длины не имеют смысла.
Поэтому вместо простого вызова utf8.length для получения длины входной строки UTF-8 и передачи этого значения в MultiByteToWideChar лучше проверить реальный размер size_t, убедившись, что преобразование в int будет безопасным и имеющим смысл, и только потом передавать его в MultiByteToWideChar.
Следующий код позволяет проверить, что длина size_t не превысит максимальное значение для переменной типа int, и сгенерировать исключение, если такое превышение есть:
if (utf8.length() > static_cast<size_t>(
std::numeric_limits<int>::max()))
{
throw std::overflow_error(
"Input string too long:
size_t-length doesn't fit into int.");
}
Обратите внимание на использование шаблона класса std::numeric_limits (из стандартного заголовочного файла <limits> в C++) для запроса наибольшего возможного значения для типа int. Однако этот код на самом деле может не скомпилироваться. В чем дело? Проблема в определении макросов min и max в заголовочных файлах Windows Platform SDK. В частности, специфичное для Windows определение макроса max препроцессора конфликтует с вызовом функции-члена std::numeric_limits<int>::max. Предотвратить это можно несколькими способами.
Возможное решение — указать #define NOMINMAX до включения <Windows.h>. Это исключит определение специфичных для Windows макросов препроцессора min и max. Однако отсутствие определения этих макросов на самом деле может вызвать проблемы с другими заголовочными файлами Windows такими как <gdiplus.h>, в котором нужны определения этих специфичных для Windows макросов.
Поэтому другой вариант — использовать дополнительную пару скобок вокруг вызова функции-члена std::numeric_limits::max, чтобы исключить раскрытие вышеупомянутых макросов:
if (utf8.length() > static_cast<size_t>((
std::numeric_limits<int>::max)()))
{
throw std::overflow_error(
"Input string too long:
size_t-length doesn't fit into int.");
}
Более того, в качестве альтернативы можно было бы использовать константу INT_MAX вместо C++-шаблона класса std::numeric_limits.
Какой бы подход вы ни выбрали, после проверки размера и анализа того, что длина значения корректна для переменной типа int, вы можете безопасно привести тип size_t к int, используя static_cast:
// Безопасное преобразование из size_t (длина STL-строки)
// в int (для Win32-функций)
const int utf8Length = static_cast<int>(utf8.length());
Заметьте, что длина строки UTF-8 измеряется восьмибитовыми единицами символов (char units), т. е. в байтах.
Первый API-вызов: получение длины конечной строки Теперь MultiByteToWideChar можно вызвать в первый раз, чтобы получить длину конечной строки UTF-16:
const int utf16Length = ::MultiByteToWideChar(
CP_UTF8, // исходная строка UTF-8
kFlags, // флаги преобразования
utf8.data(), // указатель на исходную строку UTF-8
utf8Length, // длина исходной строки UTF-8 в символах
nullptr, // не используется – на этом этапе
// преобразование не выполняется
0 // запрос размера буфера назначения в wchar_t
);
Заметьте, что функция вызывается с передачей нуля в качестве последнего аргумента. Это инструктирует MultiByteToWideChar просто вернуть необходимый размер строки назначения; на этом этапе никакое преобразование не выполняется. Также обратите внимание на то, что размер строки назначения выражается в wchar_t (не в восьмибитовых символах); это имеет смысл, так как строка назначения является Unicode-строкой в кодировке UTF-16, состоящей из последовательностей 16-битных wchar_t.
Чтобы получить доступ только для чтения к содержимому входной строки UTF-8 типа std::string, вызывается метод std::string::data. Поскольку длина строки UTF-8 явным образом передается как входной параметр, этот код будет работать и для экземпляров std::string со встроенными NUL.
Константа CP_UTF8 используется, чтобы указать кодировку входной строки в UTF-8.
Обработка ошибок Если предыдущий вызов функции закончился неудачей, например из-за наличия недопустимых для UTF-8 последовательностей во входной строке, то MultiByteToWideChar возвращает 0. В этом случае можно вызвать Win32-функцию GetLastError, чтобы выяснить детали о причине сбоя. Типичный код ошибки, возвращаемый в случае недопустимых для UTF-8 символов, — ERROR_NO_UNICODE_TRANSLATION.
При неудаче следует сгенерировать исключение. Им может быть экземпляр ранее определенного пользовательского класса Utf8ConversionException:
if (utf16Length == 0)
{
// Ошибка преобразования: захватываем код ошибки
// и генерируем исключение
const DWORD error = ::GetLastError();
throw Utf8ConversionException(
"Cannot get result string length when converting " \
"from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
error);
}
Выделение памяти для строки назначения Если вызов Win32-функции выполняется успешно, необходимая длина строки назначения содержится в локальной переменной utf16Length, поэтому можно выделить память под выходную строку UTF-16. Для строк UTF-16, Хранящихся в экземплярах класса std::wstring, достаточно вызова метода resize:
utf16.resize(utf16Length);
Заметьте: поскольку длина входной строки UTF-8 была явно передана в MultiByteToWideChar (вместо передачи –1 и запроса к этой API-функции на сканирование всей входной строки, пока не встретится завершающий NUL-символ), она не станет добавлять дополнительный завершающий NUL-символ к конечной строке. Эта API-функция будет просто обрабатывать точное количество символов во входной строке, указанное явно переданным значением длины. Следовательно, нет нужды вызывать std::wstring::resize со значением “utf16Length + 1” : так как дополнительный завершающий NUL-символ не дописывается в Win32 API, вам незачем оставлять пространство для него в строке назначения типа std::wstring (подробнее об этом см. всю ту же статью за июль 2015 года).
Второй API-вызов: выполнение преобразования Теперь, когда экземпляр UTF-16 wstring имеет достаточно пространства для конечного текста в кодировке UTF-16, можно вызвать MultiByteToWideChar во второй раз, чтобы получить реально преобразованные биты в строке назначения:
// Преобразование из UTF-8 в UTF-16
int result = ::MultiByteToWideChar(
CP_UTF8, // исходная строка UTF-8
kFlags, // флаги преобразования
utf8.data(), // указатель на исходную строку UTF-8
utf8Length, // длина исходной строки UTF-8 в символах
&utf16[0], // указатель на буфер назначения
utf16Length // размер буфера назначения в wchar_t
);
Обратите внимание на использование синтаксиса “&utf16[0]” , чтобы получить доступ для записи к внутреннему буферу памяти std::wstring (это тоже уже обсуждалось в статье за июль 2015 года).
Если первый вызов MultiByteToWideChar был успешен, вряд ли второй вызов потерпит неудачу. Тем не менее, проверка возвращаемого API-функцией значения определенно является хорошей практикой безопасного кодирования:
if (result == 0)
{
// Ошибка преобразования: захватываем код ошибки
// и генерируем исключение
const DWORD error = ::GetLastError();
throw Utf8ConversionException(
"Cannot convert from UTF-8 to UTF-16 "\
"(MultiByteToWideChar failed).",
error);
}
Иначе (в случае успеха) конечную строку UTF-16 можно наконец вернуть вызвавшему:
return utf16;
} // конец Utf8ToUtf16
Пример использования Итак, если у вас есть Unicode-строка в кодировке UTF-8 (скажем, полученная от какого-то кросс-платформенного C++-кода) и вы хотите передать ее в Win32-функцию с поддержкой Unicode, то наша функция преобразования может быть вызвана так:
std::string utf8Text = /* ...какой-то текст в UTF-8... */;
// Преобразуем из UTF-8 в UTF-16 на границе Win32 API
::SetWindowText(myWindow, Utf8ToUtf16(utf8Text).c_str());
// Примечание: в Unicode-сборках (Visual Studio по умолчанию)
// SetWindowText раскрывается в SetWindowTextW
Функция Utf8ToUtf16 возвращает экземпляр wstring, содержащий строку UTF-16, и применительно к этому экземпляру вызывается метод c_str, чтобы получить исходный указатель в стиле C на строку, завершаемую символом NUL, которая передается в Win32-функции с поддержкой Unicode.
Очень похожий код можно написать для обратного преобразования из UTF-16 в UTF-8, на этот раз вызывая API-функцию WideCharToMultiByte. Как уже отмечалось, преобразования в Unicode между UTF-8 и UTF-16 выполняются без потерь — ни один символ при преобразовании не теряется.
Библиотека преобразований Unicode-кодировок
В сопутствующий этой статье пакет исходного кода включен пример компилируемого C++-кода. Это повторно используемый код, без ошибок компилируемый в Visual C++ при уровне предупреждений 4 (/W4) в 32- и 64-разрядных сборках. Он реализован как библиотека C++ в виде только заголовочных файлов. По сути, этот модуль преобразования Unicode-кодировок состоит из двух заголовочных файлов: utf8except.h и utf8conv.h. Первый содержит определение C++-класса исключения, используемого для уведомления об ошибке при преобразованиях Unicode-кодировок. Второй реализует собственно функции преобразования Unicode-кодировок.
Заметьте, что utf8except.h содержит только кросс-платформенный C++-код; это делает возможным захват исключения при преобразовании кодировки UTF-8 в любых местах ваших проектов на C++, включая те части кода, которые не специфичны для Windows. Напротив, utf8conv.h содержит C++-код, специфичный для Windows, поскольку он напрямую взаимодействует с границей Win32 API.
Для повторного использования этого кода в ваших проектах просто включайте директивой #include эти заголовочные файлы. Сопутствующий пакет исходного кода содержит дополнительный файл, реализующий некоторые наборы тестов.
Заключение
Unicode является стандартом де-факто для представления текста на любых языках в современном программном обеспечении. Unicode-текст можно кодировать в разнообразных форматах: два наиболее важных из них — UTF-8 и UTF-16. В C++-коде для Windows часто требуется преобразовывать строки между кодировками UTF-8 и UTF-16, так как Win32-функции с поддержкой Unicode используют UTF-16 в качестве “родной” Unicode-кодировки. Текст в кодировке UTF-8 удобно хранить в экземплярах STL-класса std::string, тогда как std::wstring хорошо подходит для хранения текста в кодировке UTF-16 в C++-коде для Windows, ориентированном на компилятор Visual C++.
Win32-функции MultiByteToWideChar и WideCharToMultiByte позволяют выполнять преобразования Unicode-текста между кодировками UTF-8 и UTF-16. Я подробно описал шаблон использования функции MultiByteToWideChar, обернув ее в повторно используемую вспомогательную функцию на современном C++ для выполнения преобразований из UTF-8 в UTF-16. Обратное преобразование следует очень похожему шаблону, и повторно используемый C++-код, реализующий его, доступен в пакете кода, сопутствующем этой статье.
Джованни Диканио (Giovanni Dicanio) — программист со специализацией в области C++ и Windows, автор Pluralsight и обладатель звания Visual C++ MVP. Помимо программирования и создания учебных курсов, с удовольствием помогает другим на форумах и в сообществах, преданных C++. С ним можно связаться по адресу giovanni.dicanio@gmail.com. Также ведет блог на blogs.msmvps.com/gdicanio.
Выражаю благодарность за рецензирование статьи экспертам Дэвиду Крейви (David Cravey) и Марку Грегуа (Marc Gregoire).