Septiembre de 2016

Volumen 31, número 9

C++: conversiones de codificación Unicode con cadenas STL y API Win32

Por Giovanni Dicanio

Unicode es el estándar de facto para la representación de texto internacional en el software moderno. Según el sitio web oficial del consorcio Unicode (bit.ly/1Rtdulx), "Unicode proporciona un número único para cada carácter, independientemente de la plataforma, el programa o el idioma". Cada uno de estos números únicos se conoce como un punto de código, que normalmente se representa mediante el prefijo "U+", seguido del número único en formato hexadecimal. Por ejemplo, el punto de código asociado al carácter "C" es U+0043. Tenga en cuenta que Unicode es un estándar de la industria que abarca la mayoría de sistemas de escritura del mundo, incluidos los ideogramas. Por ejemplo, el ideograma kanji japonés 学, que tiene como significados "aprendizaje" y "conocimiento" entre otros, se asocia con el punto de código U+5B66. Actualmente el estándar Unicode define más de 1 114 000 puntos de código.

De los puntos de código abstractos a los bits reales: las codificaciones UTF-8 y UTF-16

Sin embargo, un punto de código es un concepto abstracto. Para un programador, la pregunta es: ¿cómo se representan concretamente estos puntos de código Unicode mediante bits de ordenador? La respuesta a esta pregunta lleva directamente al concepto de codificación Unicode. En esencia, una codificación Unicode es una forma concreta y bien definida de representar valores de puntos de código Unicode en bits. El estándar Unicode define varias codificaciones, pero las más importantes son UTF-8 y UTF-16, las cuales son codificaciones de longitud variable capaces de codificar todos los "caracteres" Unicode posibles o, aun mejor, puntos de código. Por consiguiente, las conversiones entre estas dos codificaciones no tienen pérdidas: ningún carácter Unicode se perderá durante el proceso.

UTF-8, como su nombre da a entender, usa unidades de código de 8 bits. Se diseñó teniendo presentes dos importantes características. La primera, que fuera retrocompatible con ASCII; esto significa que cada uno de los códigos de caracteres ASCII válidos tiene el mismo valor de byte cuando se codifica mediante UTF-8. En otras palabras, el texto en ASCII válido es automáticamente texto codificado en UTF-8 válido.

La segunda, que dado que el texto Unicode codificado en UTF-8 es solo una secuencia de unidades de byte de 8 bits, no implica una complicación respecto al formato en que se almacenan los datos (tipo de endian). Por diseño, la codificación UTF-8 (a diferencia de la UTF-16) es neutral respecto al tipo de endian. Se trata de una característica importante cuando se intercambia texto entre sistemas informáticos distintos que puedan tener arquitecturas de hardware diferentes con formatos endian distintos.

En el caso de los dos caracteres Unicode que mencioné anteriormente, la letra C mayúscula (punto de código U+0043) se codifica en UTF-8 mediante el byte único 0x43 (43 en hexadecimal), que es exactamente el código ASCII asociado al carácter C (siguiendo la retrocompatibilidad de UTF-8 con ASCII). En cambio, el ideograma japonés 学 (punto de código U+5B66) se codifica en UTF-8 como la secuencia de tres bytes 0xE5 0xAD 0xA6.

UTF-8 es la codificación Unicode más utilizada en Internet. De acuerdo con las estadísticas recientes de W3Techs disponibles en bit.ly/1UT5EBC, UTF-8 se utiliza en un 87 por ciento de todos los sitios web analizados.

UTF-16 es básicamente el estándar de codificación de facto que usan las API compatibles con Unicode de Windows. UTF-16 también es la codificación Unicode "nativa" en muchos otros sistemas de software. Por ejemplo, Qt, Java y la biblioteca International Components for Unicode (ICU), por mencionar algunos ejemplos, usan codificación UTF-16 para almacenar cadenas Unicode.

UTF-16 usa unidades de código de 16 bits. Como sucede con UTF-8, UTF-16 puede codificar todos los puntos de código Unicode posibles. No obstante, aunque UTF-8 codifica cada punto de código Unicode válido con de una a cuatro unidades de bytes de 8 bits, UTF-16 es, en cierta manera, más sencillo. De hecho, los puntos de código Unicode se codifican en UTF-16 mediante solo una o dos unidades de código de 16 bits. Sin embargo, el hecho de que las unidades de código sean mayores que un único byte implica complicaciones respecto al tipo de endian: no en vano, existen tanto UTF-16 de tipo big-endian como UTF-16 little-endian (mientras que solo hay una codificación ETF-8 neutral respecto al tipo de endian).

Unicode define un concepto de plano como un grupo continuo de 65 536 (216) puntos de código. El primer plano se identifica como plano 0 o plano multilingüe básico (BMP, Basic Multilingual Plane). Los caracteres de casi todos los idiomas modernos junto a una gran cantidad de símbolos se encuentran en el BMP, y todos estos caracteres del BMP se representan en UTF-16 mediante una única unidad de código de 16 bits.

Hay caracteres suplementarios que se encuentran en planos distintos al BMP; entre ellos se incluyen símbolos pictográficos como los Emoji y sistemas de escritura históricos como los jeroglíficos egipcios. Estos caracteres suplementarios fuera del BMP se codifican en UTF-16 mediante dos unidades de código de 16 bits, también conocidas como par suplente.

La letra C mayúscula (U+0043) se codifica en UTF-16 como una única unidad de código de 16 bits 0x0043. El ideograma 学 (U+5B66) se codifica en UTF-16 como una única unidad de código de 16 bits 0x5B66. Para muchos caracteres Unicode existe una correspondencia directa e inmediata entre la representación "abstracta" del punto de código (por ejemplo, U+5B66) y la codificación UTF-16 asociada en hexadecimal (por ejemplo, la palabra de 16 bits 0x5B66).

Como curiosidad, echemos un vistazo a algunos símbolos pictográficos. El carácter Unicode "hombre de nieve" (U+2603) se codifica en UTF-8 como una secuencia de tres bytes: 0xE2 0x98 0x83; sin embargo, su codificación en UTF-16 es la unidad de 16 bits única 0x2603. El carácter Unicode "jarra de cerveza" (U+1F37A), que se encuentra fuera del BMP, se codifica en UTF-8 mediante la secuencia de cuatro bytes 0xF0 0x9F 0x8D 0xBA. En cambio, su codificación en UTF-16 utiliza dos unidades de código de 16 bits, 0xD83C 0xDF7A, un ejemplo de un par suplente de UTF-16.

Conversión entre UTF-8 y UTF-16 mediante las API Win32

Como se ha comentado anteriormente, el texto Unicode se representa en la memoria de un equipo por medio de distintos bits, en función de la codificación Unicode concreta. ¿Qué codificación se debería usar? No hay una respuesta única a esa pregunta.

Últimamente, se considera que un buen enfoque consiste en el almacenamiento de texto Unicode codificado en UTF-8 en instancias de la clase std::string en código C++ multiplataforma. Además, existe un consenso general respecto a que UTF-8 es la codificación preferida para el intercambio de texto entre límites de aplicaciones y entre máquinas distintas. El hecho de que UTF-8 sea neutral respecto al tipo de endian es en gran parte responsable de esto. En cualquier caso, las conversiones entre UTF-8 y UTF-16 son necesarias al menos en el límite de API Win32, ya que las API compatibles con Unicode de Windows usan UTF-16 como su codificación nativa.

Ahora, trabajaremos con código C++ para implementar conversiones de codificación UTF-8/UTF-16 de Unicode. Existen dos API Win32 principales que se pueden usar para este fin: MultiByteToWideChar y su API simétrica WideCharToMultiByte. La primera se puede invocar para convertir de UTF-8 (cadena "multibyte" en la terminología de la API concreta) a UTF-16 (cadena "de caracteres anchos"); la segunda se puede utilizar para lo contrario. Como estas funciones Win32 tienen patrones de uso e interfaces similares, en este artículo solo me centraré en MultiByteToWideChar, pero he incluido código compilable de C++ que utiliza la otra API en la descarga de este artículo.

Uso de clases de cadenas STL estándares para almacenar texto Unicode Como esto es un artículo de C++, es razonable tener expectativas de almacenar texto Unicode en algún tipo de clase de cadena. Por tanto, la pregunta ahora es: ¿qué tipo de clases de cadenas de C++ se pueden usar para almacenar texto Unicode? La respuesta depende de la codificación concreta utilizada para el texto Unicode. Si se usa la codificación UTF-8, como está basada en unidades de código de 8 bits, es posible utilizar un simple elemento char para representar cada una de estas unidades de código en C++. En este caso, la clase std::string de STL, que se basa en caracteres, es una buena opción para almacenar texto Unicode codificado en UTF-8.

Por otra parte, si el texto Unicode se codifica en UTF-16, cada unidad de código se representa mediante palabras de 16 bits. En Visual C++, el tipo wchar_t tiene un tamaño de exactamente 16 bits; en consecuencia, la clase std::wstring de STL, que se basa en wchar_t, funciona correctamente para almacenar texto Unicode en UTF-16.

Merece la pena destacar que el estándar de C++ no especifica el tamaño del tipo wchar_t, por lo que aunque su tamaño es de 16 bits con el compilador de Visual C++, otros compiladores de C++ pueden utilizar tamaños diferentes. Y, de hecho, el tamaño de wchar_t que define el compilador de C++ de GNU GCC en Linux es de 32 bits. Como el tipo wchar_t tiene distintos tamaños en compiladores y plataformas diferentes, la clase std::wstring, que se basa en ese tipo, no es portable. En otras palabras, wstring se puede usar para almacenar texto Unicode en UTF-16 en Windows con el compilador de Visual C++ (que tiene un tamaño de wchar_t de 16 bits), pero no en Linux con el compilador de C++ GCC, que define un tipo wchar_t con un tamaño distinto de 32 bits.

En realidad existe otra codificación Unicode, que es menos conocida y se utiliza menos en la práctica que sus hermanas: UTF-32. Como sugiere su nombre, se basa en unidades de código de 32 bits. Por tanto, un elemento wchar_t de 32 bits de GCC o Linux es un buen candidato para la codificación UTF-32 en la plataforma Linux.

Esta ambigüedad respecto al tamaño de wchar_t determina una consecuente falta de portabilidad del código C++ basado en él (incluida la propia clase std::wstring). Por otro lado, std::string, que está basada en caracteres, es portable. Sin embargo, desde una perspectiva práctica, merece la pena resaltar que el uso de wstring para almacenar texto codificado en UTF-16 es una opción perfectamente válida para código C++ específico de Windows. De hecho, estas partes de código ya interactúan con las API Win32, que por supuesto son, por definición, específicas de la plataforma. Es por eso que aunque se incorpore wstring, la situación no cambia.

Por último, es importante destacar que, puesto que tanto UTF-8 como UTF-16 son codificaciones de longitud variable, en general los valores devueltos de los métodos string::length y wstring::length no se corresponden con el número de caracteres Unicode (o puntos de código) almacenados en las cadenas.

Interfaz de la función de conversión Se desarrollará una función para convertir texto Unicode codificado en UTF-8 a un texto equivalente codificado mediante UTF-16. Esto puede resultar útil, por ejemplo cuando se trabaja con código C++ multiplataforma que almacena cadenas Unicode codificadas en UTF-8 mediante la clase std::string de STL, y quiere pasar ese texto a API Win32 compatibles con Unicode, que normalmente usan la codificación UTF-16. Como este código se comunica con las API Win32, no es portable, por lo que en este caso std::wstring es una buena opción para almacenar texto en UTF-16. Un prototipo de función posible es el siguiente:

std::wstring Utf8ToUtf16(const std::string& utf8);

Esta función de conversión toma como entrada una cadena Unicode codificada con UTF-8, que se almacena en la clase std::string de STL estándar. Como se trata de un parámetro de entrada, se pasa mediante una referencia de tipo const (const &) a la función. Como resultado de la conversión, se devuelve una cadena codificada en UTF-16, almacenada en una instancia de std::wstring. Sin embargo, durante las conversiones de codificación Unicode, las cosas pueden no salir bien. Por ejemplo, la cadena UTF-8 de entrada podría contener una secuencia UTF-8 no válida (que podría ser el resultado de un error en otras partes del código o podría terminar ahí como consecuencia de alguna actividad maliciosa). En tales casos, lo mejor desde una perspectiva de seguridad es no realizar la conversión, en lugar de consumir secuencias de bytes potencialmente peligrosas. La función de conversión puede controlar casos de secuencias de entrada UTF-8 no válidas mediante la generación de una excepción de C++.

Definición de una clase de excepción para los errores de conversión ¿Qué tipo de clase de C++ puede usarse para generar una excepción si se produce un error en la conversión de codificaciones Unicode? Una opción podría ser utilizar una clase ya definida en la biblioteca estándar, como por ejemplo: std::runtime_error. No obstante, mi preferencia pasa por definir una nueva clase de excepción de C++ personalizada para ese propósito, que derive de std::runtime_error. Cuando las API Win32 como MultiByteToWideChar no se ejecutan correctamente, existe la posibilidad de llamar a GetLastError para obtener más información sobre el origen del error. Por ejemplo, si existen secuencias UTF-8 no válidas en la cadena de entrada, un código de error habitual que devolvería GetLastError sería ERROR_NO_UNICODE_­TRANSLATION. Es pertinente agregar esta información a la clase de excepción de C++ personalizada; puede resultar de utilidad más tarde para las tareas de depuración. La definición de esta clase de excepción puede comenzar como se indica a continuación:

// utf8except.h
#pragma once
#include <stdint.h>   // for uint32_t
#include <stdexcept>  // for std::runtime_error
// Represents an error during UTF-8 encoding conversions
class Utf8ConversionException
  : public std::runtime_error
{
  // Error code from GetLastError()
  uint32_t _errorCode;

Tenga en cuenta que el valor que devuelve GetLastError es de tipo DWORD, que representa un entero sin signo de 32 bits. Sin embargo, DWORD es un elemento typedef no portable específico de Win32. Incluso aunque esta clase de excepción de C++ se genere desde partes de código C++ específicas de Win32, es posible capturarlas mediante código C++ multiplataforma. Por tanto, tiene sentido utilizar elementos typedef portables en lugar de unos específicos de Win32; uint32_t es un ejemplo de esos tipos.

A continuación, es posible definir un constructor para inicializar instancias de esta clase de excepción personalizada con un mensaje de error y un código de error:

public:
  Utf8ConversionException(
    const char* message,
    uint32_t errorCode
  )
    : std::runtime_error(message)
    , _errorCode(errorCode)
  { }

Por último, es posible definir un captador público para ofrecer acceso de solo lectura al código de error:

uint32_t ErrorCode() const
  {
    return _errorCode;
  }}; // Exception class

Como esta clase se deriva de std::runtime_error, es posible llamar al método what para obtener el mensaje de error que se ha pasado en el constructor. Observe que en la definición de esta clase solo se han utilizado elementos estándares portables, por lo que esta clase se puede consumir sin problemas en partes de código C++ multiplataforma, incluso en los que estén ubicados lejos del punto de generación específico de Windows.

Conversión de UTF-8 a UTF-16: MultiByteToWideChar en acción

Ahora que ya se ha definido el prototipo de la función de conversión y se ha implementado una clase de excepción de C++ personalizada para representar correctamente los errores de conversión de UTF-8, es momento de desarrollar el cuerpo de la función de conversión. Como ya había adelantado, el trabajo de conversión de UTF-8 a UTF-16 se puede realizar mediante la API Win32 MultiByte­ToWideChar. Los términos "multibyte" y "de caracteres anchos" tienen su origen en hechos históricos. Básicamente, inicialmente esta API y su API simétrica WideCharToMultiByte estaban ideadas para convertir entre texto almacenado en páginas de códigos concretas y texto Unicode, que usa la codificación UTF-16 en las API Win32 compatibles con Unicode. Caracteres anchos hace referencia a wchar_t, y por tanto se asocia a una cadena basada en wchar_t, que es una cadena con codificación UTF-16. En cambio, una cadena multibyte es una secuencia de bytes expresada en una página de códigos. El concepto heredado de página de códigos se extendió después para que incluyese la codificación UTF-8.

Un patrón de uso habitual de esta API consiste en llamar primero a Multi­ByteToWideChar para obtener el tamaño de la cadena resultante. A continuación, se asigna una parte de búfer de cadenas en función del valor de ese tamaño. Normalmente se realiza mediante el método std::wstring::resize si el destino es una cadena UTF-16 (para obtener más información, es posible que quiera leer mi artículo de julio de 2015, "Using STL Strings at Win32 API Boundaries" (Uso de cadenas STL en límites de API Win32) disponible en msdn.com/magazine/mt238407). Por último, se invoca por segunda vez la función MultiByteToWideChar para realizar la conversión de codificación real, mediante el búfer de la cadena de destino que se asignó anteriormente. Observe que el mismo patrón de uso se aplica a la API simétrica WideCharToMultiByte.

Se implementará este patrón en código C++, dentro del cuerpo de la función de conversión Utf8ToUtf16 personalizada. Comience controlando el caso especial de una cadena de entrada vacía, en el que solo se devuelve un elemento wstring de salida vacío:

#include <Windows.h> // For Win32 APIs
#include <string>    // For std::string and std::wstring
std::wstring Utf8ToUtf16(const std::string& utf8)
{
  std::wstring utf16; // Result
  if (utf8.empty())
  {
    return utf16;
  }

Marcas de conversión Es posible llamar a MultiByteToWideChar por primera vez para obtener el tamaño de la cadena UTF-16 de destino. Esta función Win32 tiene una interfaz relativamente compleja y su comportamiento se define de acuerdo a unas marcas. Como se llamará dos veces a esta API en el cuerpo de la función de conversión Utf8ToUtf16, se recomienda como buenas prácticas de mantenimiento y legibilidad del código definir una constante con nombre que se pueda usar en las dos llamadas:

// Safely fails if an invalid UTF-8 character
// is encountered in the input string
constexpr DWORD kFlags = MB_ERR_INVALID_CHARS;

También resulta una buena práctica desde una perspectiva de seguridad que el proceso de conversión no se realice si se encuentra una secuencia UTF-8 no válida en la cadena de entrada. El uso de la marca MB_ERR_INVALID_CHARS también se recomienda en el libro de los autores Michael Howard y David LeBlanc, "Writing Secure Code, Second Edition" (Microsoft Press, 2003).

Si el proyecto utiliza una versión del compilador de Visual C++ más antigua que no admita la palabra clave constexpr, puede reemplazar static const en ese contexto.

Longitudes de cadena y conversiones seguras de size_t a int MultiByteToWideChar espera que el parámetro de longitud de la cadena de entrada se exprese mediante el tipo int, mientras que el método length de las clases de cadenas STL devuelve un valor de tipo equivalente a size_t. En las compilaciones de 64 bits, el compilador de Visual C++ emite una advertencia que indica una pérdida potencial de datos en la conversión de size_t (cuyo tamaño es de 8 bytes) a int (que tiene un tamaño de 4 bytes). Pero incluso en las compilaciones de 32 bits, en las que el compilador de Visual C++ define size_t e int como enteros de 32 bits, existe una falta de coincidencia respecto a la presencia o ausencia de signo: size_t no tiene signo, mientras que int sí lo tiene. Eso no es un problema para las cadenas de una longitud razonable, pero en las cadenas gigantescas con una longitud superior a (231-1), lo que corresponde a más de dos mil millones de bytes de tamaño, la conversión de un entero sin signo (size_t) a un entero con signo (int) puede generar un número negativo; y las longitudes negativas no tienen sentido.

Por ello, en lugar de realizar simplemente una invocación a utf8.length para obtener el tamaño de la cadena de entrada UTF-8 de origen y pasarlo a la API MultiByteTo­WideChar, es mejor comprobar el valor size_t real de la longitud, asegurándose de que se pueda convertir de forma segura y significativa a un tipo int, para que solamente entonces se pase a la API MultiByteToWideChar.

El código siguiente se puede usar para garantizar que la longitud de size_t no supera el valor máximo de una variable de tipo int y generaría una excepción si lo supera:

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.");
}

Observe que se ha utilizado la plantilla de la clase std::numeric_limits (del encabezado estándar de C++ <limits>) para consultar el valor más grande posible de tipo int. No obstante, cabe la posibilidad de que este código no se compilase. ¿Cómo es posible? El problema reside en la definición de las macros min y max de los encabezados del SDK de la plataforma de Windows. En concreto, la definición específica de Windows de la macro de preprocesador max entra en conflicto con la llamada a la función de miembro std::numeric_limits<int>::max. Existen varias formas de evitarlo.

Una posible solución sería utilizar #define NOMINMAX antes de incluir <Windows.h>. Esto impediría la definición de las macros de preprocesador específicas de Windows min y max. No obstante, al impedir la definición de estas macros podrían provocarse problemas con otros encabezados de Windows, como <gdiplus.h>, que requieren las definiciones de estas macros específicas de Windows.

Así, otra opción es usar un par de paréntesis adicionales alrededor de la llamada a la función de miembro std::numeric_limits::max, para evitar la expansión de macro mencionada anteriormente:

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.");
}

Además, como alternativa, se podría usar la constante INT_MAX en lugar de la plantilla de la clase de C++ std::numeric_limits.

Sea cual sea el enfoque que se aplique, una vez se haya realizado la comprobación de tamaño y se encuentre el valor de longitud adecuado para una variable de tipo int, la conversión de size_t a int se puede realizar de forma segura mediante static_cast:

// Safely convert from size_t (STL string's length)
// to int (for Win32 APIs)
const int utf8Length = static_cast<int>(utf8.length());

Observe que la longitud de la cadena UTF-8 se mide en unidades de caracteres de 8 bits; es decir, en bytes.

Primera llamada API: obtención de la longitud de la cadena de destino Ahora es posible llamar a MultiByteToWideChar por primera vez para obtener la longitud de la cadena UTF-16 de destino:

const int utf16Length = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of the source UTF-8 string, in chars
  nullptr,       // Unused - no conversion done in this step
  0              // Request size of destination buffer, in wchar_ts
);

Observe cómo se invoca la función pasando cero como el último argumento. Esto indica a la API MultiByteToWideChar que simplemente devuelva el tamaño requerido para la cadena de destino; no se realiza ninguna conversión en este paso. Observe también que el tamaño de la cadena de destino se expresa en wchar_ts (no en caracteres de 8 bits), lo que tiene sentido, ya que la cadena de destino es una cadena codificada en UTF-16, compuesta de secuencias de wchar_ts de 16 bits.

Para obtener acceso de solo lectura al contenido de std::string UTF-8 de entrada, se llama al método std::string::data. Como la longitud de la cadena UTF-8 se pasa explícitamente como un parámetro de entrada, este código también funcionará para instancias de std::string con NUL insertados dentro.

Observe también el uso de la constante CP_UTF8 para especificar que la cadena de entrada está codificada en UTF-8.

Controlar el caso en que se produzcan errores Si se produce un error en la llamada a la función anterior, por ejemplo si existen secuencias UTF-8 no válidas en la cadena de entrada, la API MultiByteToWideChar devuelve cero. En este caso, es posible invocar la función Win32 GetLast­Error para obtener más información sobre los motivos del error. Un código de error habitual si hay caracteres UTF-8 no válidos es ERROR_NO_UNICODE_TRANSLATION.

Si se produce un error, será necesario generar una excepción. Lo que se muestra a continuación puede ser una instancia de la clase Utf8Conversion­Exception que se diseñó de forma personalizada antes:

if (utf16Length == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot get result string length when converting " \
    "from UTF-8 to UTF-16 (MultiByteToWideChar failed).",
    error);
}

Asignación de memoria para la cadena de destino Si la llamada a la función Win32 se realiza correctamente, la longitud de la cadena de destino necesaria se almacena en la variable local utf16Length, de forma que se pueda asignar la memoria de destino para la cadena UTF-16 de salida. Para las cadenas UTF-16 almacenadas en instancias de la clase std::wstring, una llamada sencilla al método resize funcionaría correctamente:

utf16.resize(utf16Length);

Tenga en cuenta que como la longitud de la cadena UTF-8 de entrada se pasó explícitamente a MultiByteToWideChar (en lugar de pasar simplemente -1 y solicitar a la API que examine toda la cadena de entrada hasta que se encuentre un terminador NUL), la API Win32 no agregará un terminador NUL adicional a la cadena resultante: la API simplemente procesará el número exacto de caracteres de la cadena de entrada especificada mediante el valor de longitud pasado explícitamente. Por lo tanto, no hay necesidad de invocar a std::wstring::resize con un valor "utf16Length + 1": Como la API Win32 no incluirá ningún terminador NUL, no tendrá que reservar espacio para él dentro del elemento std::wstring de destino (puede obtener más detalles sobre esto en mi artículo de julio de 2015).

Segunda llamada API: realización de la conversión en sí Ahora que la instancia de wstring UTF-16 tiene espacio suficiente para alojar el texto codificado en UTF-16 resultante, por fin es momento de llamar a MultiByteToWideChar por segunda vez, para obtener los bits realmente convertidos en la cadena de destino:

// Convert from UTF-8 to UTF-16
int result = ::MultiByteToWideChar(
  CP_UTF8,       // Source string is in UTF-8
  kFlags,        // Conversion flags
  utf8.data(),   // Source UTF-8 string pointer
  utf8Length,    // Length of source UTF-8 string, in chars
  &utf16[0],     // Pointer to destination buffer
  utf16Length    // Size of destination buffer, in wchar_ts          
);

Observe el uso de la sintaxis "&utf16[0]" para ganar acceso de escritura al búfer de memoria interno de std::wstring (esto también se trató en mi artículo de julio de 2015).

Si la primera llamada a MultiByteToWideChar se realiza correctamente, es poco probable que en esta segunda llamada se produzcan errores. Aun así, la comprobación del valor devuelto de la API es una buena práctica de programación segura:

if (result == 0)
{
  // Conversion error: capture error code and throw
  const DWORD error = ::GetLastError();
  throw Utf8ConversionException(
    "Cannot convert from UTF-8 to UTF-16 "\
    "(MultiByteToWideChar failed).",
    error);
}

Si no, en caso de que se realice correctamente, la cadena UTF-16 resultante puede por fin devolverse al llamador:

 

return utf16;
} // End of Utf8ToUtf16

Ejemplo de uso Por lo tanto, si dispone de una cadena Unicode codificada en UTF-8 (por ejemplo, proveniente de código multiplataforma de C++) y quiere pasarla a una API Win32 compatible con Unicode, es posible invocar esta función de conversión personalizada como sigue:

std::string utf8Text = /* ...some UTF-8 Unicode text ... */;
// Convert from UTF-8 to UTF-16 at the Win32 API boundary
::SetWindowText(myWindow, Utf8ToUtf16(utf8Text).c_str());
// Note: In Unicode builds (Visual Studio default) SetWindowText
// is expanded to SetWindowTextW

La función Utf8ToUtf16 devuelve una instancia de wstring que contiene la cadena codificada en UTF-16, y se invoca el método c_str en esta instancia para obtener un puntero sin formato a una cadena terminada en NUL, que se pasará a las API Win32 compatibles con Unicode.

Es posible escribir código muy similar para la conversión inversa de UTF-16 a UTF-8, en esta ocasión con una llamada a la API WideCharToMultiByte. Como indiqué anteriormente, las conversiones Unicode entre UTF-8 y UTF-16 no tienen pérdidas: no se perderán caracteres durante el proceso de conversión.

Biblioteca de conversión de codificaciones Unicode

Se incluye código en C++ compilable en el archivo descargable que acompaña a este artículo. Se trata de código reutilizable, que se compila sin mensajes con el nivel de advertencia 4 de Visual C++ (/W4) en las compilaciones de 32 y 64 bits. Está implementado como una biblioteca de C++ solo de encabezados. Básicamente, este módulo de conversión de codificaciones Unicode consta de dos archivos de encabezados: utf8except.h y utf8conv.h. El primero contiene la definición de una clase de excepción de C++ utilizada para indicar condiciones de error durante las conversiones de codificaciones Unicode. El segundo implementa las propias funciones de conversión de codificaciones Unicode.

Tenga en cuenta que utf8except.h solo contiene código C++ multiplataforma, lo que hace posible que se capture la excepción de conversión de codificaciones UTF-8 en cualquier lugar de los proyectos de C++, incluidas las partes de código que no son específicas de Windows, y en su lugar usa C++ multiplataforma por diseño. En cambio, utf8conv.h contiene código C++ que es específico de Windows, ya que interactúa directamente con el límite de la API Win32.

Para reutilizar este código en sus proyectos, simplemente especifique #include con los archivos de encabezados que hemos comentado. El archivo descargable también contiene un archivo de código fuente adicional que implementa algunos casos de prueba.

Resumen

Unicode es el estándar de facto para la representación de texto internacional en el software moderno. El texto Unicode se puede codificar en diversos formatos: los dos más importantes son UTF-8 y UTF-16. En código C++ de Windows suele haber una necesidad de conversión entre UTF-8 y UTF-16, ya que las API Win32 compatibles con Unicode utilizan UTF-16 como su codificación Unicode nativa. El texto en UTF-8 se puede almacenar de forma conveniente en instancias de la clase std::string de STL, aunque std::wstring está preparada para almacenar texto codificado en UTF-16 en código C++ de Windows destinado al compilador de Visual C++.

Las API Win32 MultiByteToWideChar y WideCharTo­MultiByte se pueden usar para realizar conversiones entre texto Unicode representado mediante las codificaciones UTF-8 y UTF-16. He mostrado una descripción detallada del patrón de uso de la API MultiByteTo­WideChar, que he encapsulado en una función auxiliar de C++ moderna y reutilizable para realizar conversiones de UTF-8 a UTF-16. La función inversa sigue un patrón muy similar; se incluye código C++ reutilizable que la implementa en la descarga de este artículo.


Giovanni Dicanio es un programador informático especializado en C++ y Windows, autor de Pluralsight y MVP de Visual C++. Además de la programación y la creación de cursos, disfruta ayudando a los demás en foros y comunidades dedicadas a C++; puede ponerse en contacto con él en giovanni.dicanio@gmail.com. También escribe un blog en blogs.msmvps.com/gdicanio.

Gracias a los siguientes expertos técnicos por revisar este artículo: David Cravey y Marc Gregoire
David Cravey trabaja como arquitecto empresarial en GlobalSCAPE, dirige varios grupos de usuarios de C++ y ha sido cuatro veces MVP de Visual C++.

Marc Gregoire es un ingeniero de software senior de Bélgica, fundador del grupo belga de usuarios de C++, autor de "Professional C++" (Wiley), coautor de "C++ Standard Library Quick Reference" (Apress), editor técnico en numerosos libros y, desde 2007, ha recibido el premio anual MVP por sus conocimientos en VC++. Puede ponerse en contacto con Marc en marc.gregoire@nuonsoft.com.