Setembro de 2016

Volume 31 – Número 9

C++ – conversões de codificação Unicode com cadeias de caracteres STL e APIs Win32

De Giovanni Dicanio

O Unicode é o padrão de fato para a representação de texto internacional em software moderno. De acordo com o site oficial do consórcio Unicode (bit.ly/1Rtdulx), “O Unicode oferece um número exclusivo para todos os caracteres, independentemente da plataforma, do programa e da linguagem”. Cada um desses números exclusivos é chamado de ponto de código e normalmente é representado com o prefixo “U+”, seguido pelo número exclusivo escrito na forma hexadecimal. Por exemplo, o ponto de código associado ao caractere “C” é U+0043. Observe que o Unicode é um padrão do setor que aborda a maioria dos sistemas de escrita do mundo, incluindo os ideogramas. Dessa forma, por exemplo, o ideograma kanji japonês 学, que tem “aprendizado” e “conhecimento” entre seus significados, está associado ao ponto de código U+5B66. Atualmente, o padrão Unicode define mais de 1.114.000 pontos de código.

Dos Pontos de Código Abstratos aos Bits Reais: Codificações UTF-8 e UTF-16

No entanto, um ponto de código é um conceito abstrato. Para um programador, a dúvida é: Como estes pontos de código Unicode são representados de forma concreta usando bits de computador? A resposta leva diretamente ao conceito de codificação Unicode. Basicamente, uma codificação Unicode é uma maneira particular e bem-definida de representar os valores de ponto de código Unicode em bits. O padrão Unicode define várias codificações, mas as mais importantes são UTF-8 e UTF-16, e ambas são codificações de tamanho variável capazes de codificar todos os “caracteres” Unicode possíveis, ou melhor, os pontos de código. Dessa forma, as conversões entre essas duas codificações não têm perdas: Nenhum caractere Unicode será perdido durante o processo.

A UTF-8, como o nome sugere, usa unidades de código de 8 bits. Foi projetada com duas características importantes em mente. Primeiro, é compatível com versões anteriores, como ASCII; isso significa que cada código de caractere ASCII válido tem o mesmo valor de byte quando codificado em UTF-8. Em outras palavras, um texto ASCII válido é automaticamente um texto codificado com UTF-8 válido.

Segundo, como o texto Unicode codificado em UTF-8 é apenas uma sequência de unidades de byte de 8 bits, não há uma complicação de ordenação endian. A codificação UTF-8 (ao contrário da UTF-16) tem ordenação com neutralidade de endian por design. Esse é um recurso importante ao trocar texto entre sistemas de computação diferentes que possam ter arquiteturas de hardware diferentes com ordenação endian diferente.

Considerando os dois caracteres I Unicode mencionados anteriormente, a letra C maiúscula (ponto de código U+0043) é codificada em UTF-8 usando o byte único 0x43 (43 hexadecimal), que é exatamente o código ASCII associado ao caractere C (de acordo com a compatibilidade com versões anteriores da UTF-8 com ASCII). Em contrapartida, o ideograma japonês 学 (ponto de código U+5B66) é codificado em UTF-8 como a sequência de três bytes 0xE5 0xAD 0xA6.

A UTF-8 é a codificação Unicode mais usada na Internet. De acordo com estatísticas recentes da W3Techs, disponíveis em bit.ly/1UT5EBC, a UTF-8 é usada por 87% de todos os sites analisados.

A UTF-16 é, basicamente, a codificação padrão de fato usada pelas APIs habilitadas para Unicode do Windows. A UTF-16 também é a codificação Unicode “nativa” em vários outros sistemas de software. Por exemplo, o Qt, o Java e a biblioteca ICU (International Components for Unicode), só para mencionar alguns, usam a codificação UTF-16 para armazenar cadeias de caracteres Unicode.

A UTF-16 usa unidades de código de 16 bits. Assim como a UTF-8, a UTF-16 pode codificar todos os pontos de código Unicode possíveis. Entretanto, enquanto a UTF-8 codifica cada ponto de código Unicode válido usando de uma a quatro unidades de byte de 8 bits, a UTF-16 é, de certa forma, mais simples. Na verdade, os pontos de código Unicode são codificados em UTF-16 usando apenas uma ou duas unidades de código de 16 bits. No entanto, ter unidades de código maiores do que um byte único implica complicações de ordenação endian: na verdade, há a UTF-16 big-endian e UTF-16 little-endian (enquanto há apenas uma codificação UTF-8 com neutralidade de endian).

O Unicode define um conceito de plano como um grupo contínuo de 65.536 (216) pontos de código. O primeiro plano é identificado como plano 0 ou como BMP (Basic Multilingual Plane). Os caracteres de quase todos os idiomas modernos e diversos símbolos estão localizados no BMP, e todos esses caracteres BMP são representados na UTF-16 usando uma única unidade de código de 16 bits.

Os caracteres suplementares estão localizados em planos diferentes do BMP; eles incluem símbolos pictográficos como Emojis e scripts históricos, como os hieróglifos egípcios. Esses caracteres suplementares fora do BMP são codificados em UTF-16 usando duas unidades de código de 16 bits, também conhecidas como pares alternativos.

A letra C maiúscula (U+0043) é codificada em UTF-16 como uma única unidade de código de 16 bits, 0x0043. O ideograma 学 (U+5B66) é codificado em UTF-16 como uma única unidade de código de 16 bits, 0x5B66. Para vários caracteres Unicode, há uma correspondência direta e imediata entre sua representação “abstrata” de ponto de código (como U+5B66) e sua codificação em UTF-16 associada em hexadecimal (por exemplo, a palavra de 16 bits 0x5B66).

Para nos divertimos um pouco, vejamos alguns símbolos pictográficos. O caractere Unicode “boneco de neve” (, U+2603) é codificado em UTF-8 como a sequência de três bytes: 0xE2 0x98 0x83; no entanto, sua codificação UTF-16 é a unidade única 0x2603 de 16 bits. O caractere Unicode “caneca de cerveja” (, U+1F37A), que está localizado fora do BMP, é codificado em UTF-8 pela sequência de 4 bytes 0xF0 0x9F 0x8D 0xBA. Em vez disso, sua codificação UTF-16 usa duas unidades de código de 16 bits, 0xD83C 0xDF7A, um exemplo de par alternativo UTF-16.

Conversão entre UTF-8 e UTF-16 usando APIs Win32

Como discutido nos parágrafos anteriores, o texto Unicode é representado na memória de um computador usando bits diferentes, com base na codificação Unicode particular. Qual codificação você deve usar? Não há uma resposta única para essa pergunta.

Recentemente, a sabedoria convencional parece sugerir que uma boa abordagem consiste em armazenar texto Unicode codificado em UTF-8 em instâncias da classe std::string em código C++ de plataforma cruzada. Além disso, todos concordam que a UTF-8 é a codificação preferencial para trocar texto entre limites de aplicativo e entre computadores diferentes. Acontece que a UTF-8 é um formato com neutralidade de endian que tem um função importante nisso. Em qualquer caso, as conversões entre UTF-8 e UTF-16 são necessárias pelo menos no limite da API Win32, já que as APIs habilitadas para Unicode do Windows usam a UTF-16 como sua codificação nativa.

Agora, vamos examinar mais de perto um código C++ para implementar essas conversões de codificação UTF-8/UTF-16 do Unicode. Há duas APIs Win32 fundamentais que podem ser usadas para esta finalidade: MultiByteToWideChar e sua simétrica WideCharToMultiByte. A primeira pode ser invocada para converter de UTF-8 (cadeia de caracteres de “vários bytes” na terminologia específica da API) em UTF-16 (cadeia de “caracteres amplos”); a última pode ser usada para o oposto. Como as funções Win32 têm interfaces e padrões de uso semelhantes, me concentrarei em MultiByteToWideChar neste artigo, mas incluí um código C++ compilável que usa a outra API como parte do download deste artigo.

Usando as classes string STL padrão para armazenar texto unicode Como esse é um artigo de C++, há uma expectativa válida de armazenar texto Unicode em algum tipo de classe string. Dessa forma, a dúvida agora é: Que tipo de classes string do C++ pode ser usado para armazenar texto Unicode? A resposta se baseia na codificação particular usada para o texto Unicode. Se a codificação UTF-8 for usada, como ela se baseia em unidades de código de 8 bits, um caractere simples pode ser usado para representar cada uma dessas unidades de código em C++. Nesse caso, a classe std::string STL, que é baseada em caracteres, é uma boa opção para armazenar código Unicode codificado em UTF-8.

Por outro lado, e o texto Unicode for codificado em UTF-16, cada unidade de código será representada por palavras de 16 bits. No Visual C++, o tipo wchar_t tem exatamente 16 bits de tamanho; consequentemente, a classe std::wstring STL, que se baseia em wchar_t, funciona bem para armazenar texto Unicode UTF-16.

Vale a pena observar que o padrão C++ não especifica o tamanho do tipo wchar_t e, portanto, enquanto ele tem 16 bits com o compilador do Visual C++, outros comiladores do C++ podem usar tamanhos diferentes. E, na verdade, o tamanho do wchar_t definido pelo compilador C++ GCC GNU no Linux é de 32 bits. Como o tipo wchar_t tem tamanhos diferentes em compiladores e plataformas diferentes, a classe std::wstring, que se baseia nesse tipo, não é portável. Em outras palavras, wstring pode ser usado para armazenar texto Unicode codificado em UTF-16 no Windows com o compilador Visual C++ (onde o tamanho de wchar_t é de 16 bits), mas não no Linux com o compilador C++ GCC, que define um tipo de wchar_t de 32 bits de tamanho diferente.

Na verdade, há outra codificação Unicode, que é menos conhecida e menos usada na prática do que suas irmãs: a UTF-32. Como seu nome sugere claramente, ela se baseia em unidades de código de 32 bits. Dessa forma, uma wchar_t de 32 bits GCC/Linux é um bom candidata para a codificação UTF-32 na plataforma Linux.

Essa ambiguidade no tamanho de wchar_t determina uma consequente falta de portabilidade do código C++ baseado nela (incluindo a própria classe std::wstring). Por outro lado, std::string, que se baseia em caractere, é portável. Entretanto, de uma perspectiva prática, vale a pena observar que o uso de wstring para armazenar texto codificado em UTF-16 também funciona bem em código C++ específico do Windows. Na verdade, essas partes de código já interagem com APIs Win32, que são, é claro específicas da plataforma por definição. Dessa forma, adicionar wstring à mistura não muda a situação.

Por fim, é importante observar que, porque a UTF-8 e a UTF-16 são codificações de tamanho variável, os valores de retorno dos métodos string::length e wstring::length em geral não correspondem ao número de caracteres Unicode (ou aos pontos de código) armazenados nas cadeias de caracteres.

A Interface de Função de Conversão Vamos desenvolver uma função para converter texto Unicode codificado em UTF-8 no texto equivalente codificado usando UTF-16. Isso poderá ser útil, por exemplo, quando você tiver código C++ de plataforma cruzada que armazene cadeias de caracteres Unicode codificada em UTF-8 usando a classe std::string STL e quiser passar o texto para APIs Win32 habilitadas para Unicode, que normalmente usam a codificação UTF-16. Como esse código está conversando com APIs Win32, ainda é não portável e, portanto, std::wstring é bastante adequado a armazenar texto UTF-16 aqui. Um possível protótipo de função será:

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

Essa função de conversão obtém como entrada uma cadeia de caracteres codificada em UTF-8 Unicode, que é armazenada na classe std::string STL padrão. Como esse é um parâmetro de entrada, ele é passado por referência const (const &) para a função. Como o resultado da conversão, uma cadeia de caracteres codificada em UTF16 é retornada, armazenada em uma instância de std::wstring. Entretanto, durante as conversões de codificação Unicode, algo pode dar errado. Por exemplo, a cadeia de caracteres UTF-8 de entrada pode conter uma sequência UTF-8 inválida (que pode ser o resultado de um bug em outras partes do código ou poderia terminar ali como resultado de alguma atividade mal-intencionada). Nesses casos, a melhor opção da perspectiva da segurança é falhar na conversão, em vez de consumir sequências de bytes potencialmente perigosas. A função de conversão pode lidar com casos de sequências de entrada UTF-8 inválidas ao lançar uma exceção C++.

Definição de uma classe de exceção para erros de conversão Que tipo de classe do C++ pode ser usado para lançar uma exceção no caso de uma falha de conversão de codificação Unicode? Uma opção pode ser usar uma classe já definida na biblioteca padrão, por exemplo: std::runtime_error. Entretanto, eu prefiro definir uma nova classe de exceção do C++ personalizada para essa finalidade, derivada de std::runtime_error. Quando APIs Win32 como MultiByteToWideChar falham, é possível chamar GetLastError para obter mais informações sobre a causa da falha. Por exemplo, em caso de sequências UTF-8 inválidas na cadeia de caracteres de entrada, um código de erro típico retornado por GetLastErorr é ERROR_NO_UNICODE_­TRANSLATION. Faz sentido adicionar essas informações à classe de exceção do C++ personalizada; elas poderão ser úteis posteriormente para fins de depuração. A definição dessa classe de exceção pode começar desta forma:

// 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;

Observe que o valor retornado por GetLastError é do tipo DWORD, que representa um inteiro não assinado de 32 bits. Entretanto, DWORD é um typedef não portável específico de Win32. Mesmo se essa classe de exceção do C++ for lançada de partes específicas do Win32 de código C++, ela poderá ser capturada pelo código C++ de plataforma cruzada! Dessa forma, faz sentido usar typedefs portáveis em vez de específicos do Win32; o uint32_t é um exemplo de tais tipos.

Em seguida, um construtor pode ser definido para inicializar instâncias dessa classe de exceção personalizada com uma mensagem de erro e um código de erro:

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

Por fim, um getter público pode ser definido para fornecer acesso somente leitura ao código de erro:

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

Como essa classe deriva de std::runtime_error, é possível chamar o método what para obter a mensagem de erro passada no construtor. Observe que na definição dessa classe, foram usados somente elementos padrão portáveis e, portanto, essa classe é perfeitamente consumível em partes de plataforma cruzada do código C++, mesmo aquelas afastadas do ponto de lançamento específico do Windows.

Convertendo de UTF-8 em UTF-16: MultiByteToWideChar em ação

Agora que o protótipo da função de conversão foi definido e uma classe de exceção do C++ personalizada implementada para representar adequadamente as falhas de conversão da UTF-8, é hora de desenvolver o corpo da função de conversão. Como já foi dito, o trabalho de conversão de UTF-8 em UTF-16 pode ser feito usando a API Win32 MultiByte­ToWideChar. Os termos “multi-byte” (vários bytes) e “wide-char” (caractere amplo) têm raízes históricas. Basicamente, essa API e sua irmã simétrica WideCharToMultiByte foram inicialmente criadas para converter entre o texto armazenado em páginas de código específicas e texto Unicode, que usa a codificação UTF-16 em APIs habilitadas para Unicode Win32. O caractere amplo refere-se a wchar_t e, portanto, está associado a uma cadeia de caracteres baseada em wchar_t, que é uma cadeia de caracteres codificada em UTF-16. Em contrapartida, uma cadeia de caracteres de vários bytes é uma sequência de bytes expressa em uma página de código. O conceito herdado de página de código foi então estendido para incluir a codificação UTF-8.

Um padrão de uso típico dessa API consiste em primeiro chamar MultiByteToWideChar para obter o tamanho da cadeia de caracteres resultante. Em seguida, um buffer de cadeia de caracteres é alocado de acordo com esse valor de tamanho. Isso é normalmente feito usando o método std::wstring::resize caso o destino seja uma cadeia de caracteres UTF-16. (Para obter mais detalhes, leia meu artigo de julho de 2015, “Using STL Strings at Win32 API Boundaries” (Usando cadeias de caracteres STL em limites de API Win32), em msdn.com/magazine/mt238407). Por fim, a função MultiByteToWideChar é invocada uma segunda vez para fazer a conversão de codificação real, usando o buffer de cadeia de caracteres de destino alocado anteriormente. Observe que o mesmo padrão de uso se aplica à API simétrica WideCharToMultiByte.

Vamos implementar esse padrão em código C++, no corpo da função de conversão Utf8ToUtf16 personalizada. Comece a tratar o caso especial de uma cadeia de caracteres de entrada vazia, onde somente uma wstring de saída vazia é retornada:

#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;
  }

Sinalizadores de Conversão O MultiByteToWideChar pode ser chamado pela primeira vez para obter o tamanho da cadeia de catacteres UTF-16 de destino. Essa função Win32 tem uma interface relativamente complexa, e seu comportamento é definido de acordo com alguns sinalizadores. Como essa API será chamada duas vezes no corpo da função de conversão Utf8ToUtf16, é uma boa prática para legilibilidade e capacidade de manutenção de código para definir uma constante nomeada que pode ser usada em ambas as chamadas:

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

Também é uma boa prática de uma perspectiva de segurança para fazer o processo de conversão falhar se uma sequência UTF-8 inválida for encontrada na cadeia de caracteres de entrada. O uso do sinalizador MB_ERR_INVALID_CHARS também é incentivado no livro de Michael Howard e David LeBlanc, “Writing Secure Code, Second Edition” (Escrevendo código seguro, segunda edição) (Microsoft Press, 2003).

Se seu projeto usar uma versão mais antiga do compilador do Visual C++ que não dê suporte à palavra-chave constexpr, você poderá substituir a constante static nesse contexto.

Tamanhos de Conversões Seguras de size_t em int MultiByteToWideChar espera o parâmetro de tamanho de cadeia de caracters de entrada expressado usando o tipo int, enquanto o método length das classes STL retorna um valor do tipo equivalente a size_t. Em builds de 64 bits, o compilador do Visual C++ emite um aviso sinalizando uma potencial perda de dados para a conversão de size_t (cujo tamanho é 8 bytes) em int (com o tamanho de 4 bytes). Mas mesmo em builds de 32 bits, onde size_t e int são definidos como inteiros de 32 bits pelo compilador do Visual C++, há uma incompatibilidade entre não assinado/assinado: size_t é não assinado, e int é assinado. Isso não é um problema para cadeias de caracteres de tamanho razoável, mas para as cadeias de caracteres de tamanho maior do que (231-1) — ou seja, mais de dois bilhões de bytes de tamanho — a conversão de um inteiro não assinado (size_t) em um inteiro assinado (int) pode gerar um número negativo, e tamanhos negativos não fazem sentido.

Dessa forma, em vez de simplesmente invocar utf8.lenght para obter o tamanho da cadeia de caracteres de entrada UTF-8 de origem, e de passá-la para a API MultiByteToWideChar, é melhor verificar o real size_t-value do tamanho, garantindo que ele seja convertido de forma segura e significativa em um int, e somente então passá-lo para a API MultiByteToWideChar.

O código a seguir pode ser usado para garantir que size_t-length não exceda o valor máximo de uma variável do tipo int, lançando uma exceção caso ele:

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 o uso do modelo de classe std::numeric_limits (do cabeçalho padrão <limits> do C++) para consultar o maior valor possível para o tipo int. Entretanto, esse código pode não ser realmente compilado. Como? O problema está na definição das macros min e max nos cabeçalhos do SDK da Plataforma Windows. Em particular, a definição específica do Windows da macro do pré-processador max entra em conflito com a chamada da função membro std::numeric_limits<int>::max. Há algumas maneiras de impedir isso.

Uma possível solução é #define NOMINMAX antes de incluir <Windows.h>. Isso impedirá a definição das macros de pré-processador min e max específicas do Windows. No entanto, impedir a definição dessas macros pode realmente causar problemas com outros cabeçalhos do Windows, como <gdiplus.h>, que exigem as definições dessas macros específicas do Windows.

Dessa forma, outra opção é usar um par extra de parênteses em torno da chamada da função membro std::numeric_limits::max para impedir a expansão da 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.");
}

Além disso, como uma alternativa, a constante INT_MAX poderia ser usada em vez do modelo de classe std::numeric_limits do C++.

Seja qual for a abordagem usada, assim que a verificação do tamanho for feita e o valor de tamanho for encontrado como adequado para uma variável do tipo int, a conversão de size_t em int pode ser executada de forma segura usando 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 o tamanho da cadeia de caracteres UTF-8 é medido em unidades de caracteres de 8 bits; ou seja, em bytes.

Primeira Chamada de API: Obter o Comprimento da Cadeia de Caracteres de Destino Agora, MultiByteToWideChar pode ser chamado pela primeira vez para obter o tamanho da cadeia de caracteres 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 como a função é invocada passando zero como o último argumento. Isso instrui a API MultiByteToWideChar a simplesmente retornar o tamanho necessário à cadeia de caracteres de destino; nenhuma conversão é concluída nesta etapa. Observe também que o tamanho da cadeia de caracteres de destino é expresso em wchar_ts (não em caracteres de 8 bits), o que faz sentido, porque a cadeia de caracteres de destino é uma cadeia de caracteres Unicode codificada em UTF-16, composta por sequências de wchar_ts de 16 bits.

Para obter acesso somente leitura ao conteúdo da cadeia de caracteres std::string UTF-8, o método std::string::data é chamado. Como o tamanho da cadeia de caracteres UTF-8 é passado explicitamente como um parâmetro de entrada, esse código funcionará também para instâncias de std::string com NULs internos.

Observe também o uso da constante CP_UTF8 para especificar que a cadeia de caracteres de entrada seja codificada em UTF-8.

Tratando o Caso de Erro Se a chamada à função anterior falhar, por exemplo, na presença de sequências UTF-8 inválidas na cadeia de caracteres de entrada, a API MultiByteToWideChar retornará zero. Nesse caso, a função Win32 GetLast­Error pode ser invocada para obter mais detalhes sobre a causa da falha. Um código de erro típico retornado no caso de caracteres UTF-8 inválido é ERROR_NO_UNICODE_TRANSLATION.

Em caso de falha, é hora de lançar uma exceção. Essa pode ser uma instância da classe Utf8Conversion­Exception personalizada projetada anteriormente:

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

Alocando Memória para a Cadeia de Caracteres de Destino Se a chamada à função Win32 for bem-sucedida, o tamanho da cadeia de caracteres de destino necessária será armazenado na variável local utf16Length e, portanto, a memória de destino para a cadeia de caracteres UTF-16 de saída poderá ser alocada. Para as cadeias de caracteres UTF-16 armazenadas em instâncias da classe std::wstring, uma simples chamada ao método resize funcionaria bem:

utf16.resize(utf16Length);

Observe que como o tamanho da cadeia de caracteres UTF-8 de entrada foi explicitamente passado para MultiByteToWideChar (em vez de simplesmente passar -1 e solicitar que a API examine a cadeia de caracteres de entrada inteira até um NUL-terminator ser encontrado), a API Win32 não adicionará outro NUL-terminator à cadeia de caracteres resultante: A API simplesmente processará o número exato de caracteres na cadeia de caracteres de entrada especificada pelo valor de tamanho passado explicitamente. Portanto, não há a necessidade de invocar std::wstring::resize com um valor “utf16Length + 1”: Como nenhum NUL-terminator adicional será inserido pela API Win32, não será necessário abrir espaço para ele na std::wstring de destino (mais detalhes sobre o que pode ser encontrado em meu artigo de julho de 2015).

Segunda Chamada de API: Fazendo a Conversão Real Agora que a instância de wstring UTF-16 tem espaço suficiente para hospedar o texto codificado em UTF-16 resultante, finalmente é hora de chamar MultiByteToWideChar pela segunda vez para obter os bits reais convertidos na cadeia de caracteres 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 o uso da sintaxe “&utf16[0]” para obter acesso de gravação ao buffer de memória interna de std::wstring (isso também já foi discutido em meu artigo de julho de 2015).

Se a primeira chamada a MultiByteToWideChar for bem-sucedida, é improvável que a segunda chamada falhe. Ainda assim, verificar o valor de retorno da API é certamente uma prática de codificação boa e 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);
}

Além disso, em caso de êxito, a cadeia de caracteres UTF-16 resultante poderá finalmente ser retornada ao chamador:

 

return utf16;
} // End of Utf8ToUtf16

Amostra de Uso Dessa forma, se você tiver uma cadeia de caracteres Unicode codificada em UTF-8-(por exemplo, vindo de algum código C++ de plataforma cruzada) e se quiser passá-lo para uma API habilitada por Unicode Win32, essa função de conversão personalizada poderá ser invocada desta forma:

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

A função Utf8ToUtf16 retorna uma instância de wstring com a string codificada em UTF-16, e o método c_str é invocado nessa instância para obter um ponteiro bruto no estilo C para uma cadeia de caracteres NUL-terminated, a ser passada para APIs habilitadas para Unicode Win32.

Um código bem semelhante pode ser escrito para a conversão reversa de UTF-16 em UTF-8, dessa vez chamando a API WideCharToMultiByte. Como observei anteriormente, as conversões de Unicode entre UTF-8 e UTF-16 não têm perdas —nenhum caractere será perdido durante o processo de conversão.

Biblioteca de conversão de codificação Unicode

O código C++ compilável de exemplo é incluído no arquivo morto baixável associado a este artigo. Esse código é reutilizável, compilando de forma limpa no nível de aviso do Visual C++ 4 (/W4) nos builds de 32 bits e de 64 bits. É implementado como uma biblioteca do C++ somente cabeçalho. Basicamente, este módulo de conversão de codificação Unicode consiste em dois arquivos de cabeçalho: utf8except.h e utf8conv.h. O primeiro contém a definição da classe de exceção do C++ usada para sinalizar condições de erro durante as conversões de codificação Unicode. O último implementa as funções de conversão de codificação Unicode real.

Observe que utf8except.h contém somente código C++ de plataforma cruzada, possibilitando a captura da exceção de conversão de codificação UTF-8 em qualquer lugar em seus projetos C++, incluindo partes do código que não sejam específicas do Windows; em vez disso, use o C++ de plataforma cruzada por padrão. Em contrapartida, utf8conv.h contém código C++ específico do Windows, já que ele interage diretamente com o limite da API Win32.

Para reutilizar esse código em seus projetos, use #include para incluir os arquivos de cabeçalho mencionados anteriormente. O arquivo morto baixável contém um arquivo de origem adicional que também implementa alguns casos de teste.

Conclusão

O Unicode é o padrão de fato para a representação de texto internacional em software moderno. O texto Unicode pode ser codificado em diversos formatos: Os dois mais importantes são UTF-8 e UTF-16. No código do Windows C++, com frequência há uma necessidade de converter entre UTF-8 e UTF-16, já que as APIs Win32 habilitadas para Unicode usam UTF-16 como sua codificação Unicode nativa. O texto UTF-8 pode ser convenientemente armazenado em instâncias da classe std::string STL, enquanto std::wstring é adequada a armazenar texto codificado em UTF-16 no código Windows C++ voltado para o compilador do Visual C++.

As APIs Win32 MultiByteToWideChar e WideCharTo­MultiByte podem ser usadas para executar conversões entre o texto Unicode representado usando as codificações UTF-8 e UTF-16. Eu mostrei uma descrição detalhada do padrão de uso da API MultiByteTo­WideChar, encapsulando-a em uma função auxiliar do C++ moderna e reutilizável para executar conversões do UTF-8 em UTF-16. A conversão reversa segue um padrão bastante semelhante, e o código C++ reutilizável que a implementa está disponível no download deste artigo.


Giovanni Dicanio é programador de computador especializado em C++ e Windows, autor da Pluralsight e MVP do Visual C++. Além da programação e da criação de cursos, ele gosta de ajudar outras pessoas em fóruns e em comunidades devotados ao C++, e é possível entrar em contato com ele pelo email giovanni.dicanio@gmail.com. Ele também escreve um blog em blogs.msmvps.com/gdicanio.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: David Cravey e Marc Gregoire
David Cravey é arquiteto empresarial na GlobalSCAPE, lidera vários grupos de usuários de C++ e foi quatro vezes MVP do Visual C++.

Marc Gregoire é um engenheiro de software sênior belga, fundador do Grupo de Usuários de C++ na Bélgica, autor de “Professional C++” (C++ Profissional) (Wiley), coautor de “C++ Standard Library Quick Reference” (Referência rápida da biblioteca padrão do C++) (Apress), editor técnico de diversos livros e, desde 2007, recebeu o prêmio anual MVP por sua experiência no VC++. É possível entrar em contato com Marc pelo email marc.gregoire@nuonsoft.com.