Charset é coisa séria!

Infelizmente a maioria dos profissionais de desenvolvimento de software, especialmente a turma da web, não presta muita atenção para o conjunto de caracteres que usa. Muitos não fazem nem ideia das diferenças entre esses conjuntos e aderem ao que está à mão (WINDOWS-1252 no Windows, mas confundindo-o com o ISO-8859-1 e, UTF-8 no Linux, mas confundem com o ASCII comum).

O detalhe sobre charsets é que eles são mapas para duas coisas diferentes. A primeira é o mapa do valor numérico que representa um caracter específico. Por exemplo, o caracter ‘𝄞’, em Unicode, é definido como U+1D11E (esse é o modo “padrão” de dizer 0x1D11E, em Unicode)… A outra coisa é a maneira como esse valor é codificado. Tomando como exemplo o mesmo caracter, usando a codificação UTF-8, ele é representado como uma sequência de 4 bytes: 0xf0,  0x9d, 0x84 e 0x9e. Para mostrar a diferença, se usássemos UTF-16 (onde cada item da sequência tem 16 bits), o mesmo caracter seria codificado com a sequência 0xd834 0xdd1e. O mesmo caracter não pode ser codificado no charset WINDOWS-1252:

$ echo '𝄞' | iconv -f utf8 -t windows-1252
iconv: illegal input sequence at position 0

Ahhhh… mas você está usando Windows e conseguindo ver a “clave de sol”, não é? Acontece que todo este texto está codificado em UTF-8 e o WordPress diz para o seu browser que ele precisa usar UTF-8 ao invés de WINDOWS-1252!

“Unicode”

Unicode refere-se, ao mesmo tempo, ao mapeamento de caracteres, dividindo-os em “planos” e à maneira de codificar os códigos desses mapas. O primeiro plano da lista é o “multi-lingual básico”, que engloba os caracteres de U+0000 até U+FFFF e é dividido de acordo com a imagem abaixo (os valores nas ‘células’ correspndem ao MSB do código):

Os demais planos são suplementos, tendo caracteres que complementam os do plano básico e outros “especiais”, como “emoticones”, por exemplo. Uma descrição mais completa pode ser obtida no artigo do Wikipedia onde retirei essa imagem: aqui.

Windows versus Linux/BSD/OS-X:

Todos eles usam UNICODE, mas, no caso dos sistemas baseados em Unix listados acima, a codificação padrão é o UTF-8. No caso do Windows, há uma comportamento bem estranho devido à necessidade da Microsoft em manter compatibilidade das novas versões do Windows com as antigas… Existem DOIS sistemas de codificação padrão…

Antes do Windows NT, quem fazia sucesso era o Windows 3.0 e, depois, seus irmãos mais velhos, o Windows 3.1 e, depois, o Windows 3.11. A Microsoft usava um conjunto de caracteres que ela chamava de ANSI (não confundir com ASCII). Esse padrão é, essencialmente, o mesmo ISO-8859-1 com alguns caracteres adicionais no plano que, mais tarde, veio a ser padronizado como WINDOWS-1252. Nesses padrões (ISO e WINDOWS) cada caracter do plano é codificado em apenas 1 byte e, por isso, temos apenas 256 caracteres “possíveis”. Temos que retirar, claro, aqueles que não são “imprimíveis”, diminuindo a faixa.

Acontece que, rapidamente, a Microsoft percebeu que a adoção de um charset mais extenso era inevitável (como ficam os usuários na China? No Japão? E os Russos? E os Gregos?). Assim, para o Windows NT, que viria a ser o padrão para todos os demais Windows “desktop”, depois do Windows Millenium, adotou-se o UNICODE, mas apenas os planos, parcialmente, não as codificações… Windows não usa UTF, mas sim uma representação de 16 bits por caracter, ao invés de 8.

Essa implementação do UNICODE é chamada erroneamente de UCS-2 (que é o velho nome para a versão 1.1 do UNICODE). Quando digo que foi feita a adoção parcial dos planos do Unicode é porque, se um caracter pode ser representado em 1 dos 65536 códigos possíveis (16 bits), todos os planos não podem ser representados (UNICODE permite, atualmente, 21 bits de codificação, hipoteticamente totalizando 2097152 de caracteres, mas apenas os códigos até U+10FFFF são possíveis, totalizando 1114112 caracteres).

Mas, eu consigo usar todo o UNICODE no Windows!

Eu falei, ai em cima, que a Microsoft optou por usar “UCS-2” por default. Acontece que suporte para Multi Byte Character Sets foi adicionado ao NT para suportar UTF. Mas esse não é o padrão…

Aliás, se você desenvolve para Windows e tem muitas strings em seu código, recomendo que comece a usar ponteiros para o tipo wchar_t ou, na nomenclatura de tipos da Microsoft, LPWSTR. Da mesma forma, usar as funções da Win32 API que terminem com W, não as que terminal com A, como em CreateWindowW(), ao invés de CreateWindowA() (As funções sem esses sufixos são, normalmente, macros de mapeamento para as funções que lidam com charset ANSI, ou seja, as terminadas com A)… O motivo é o que expliquei lá em cima: Windows, desde o NT, usa, internamente, representação de 16 bits por caracter (“UCS-2”). Quando seu código usa a codificação “ANSI” as strings são sempre convertidas para o uso do Windows… Isso quer dizer que, embora suas strings fiquem duas vezes menores em ANSI, Windows leva o dobro do tempo para processá-las por ter que convertê-las… Assim, escrever:

LPWSTR lpszClassName = L"MyWindowClass";

Fará com que a string ocupe 28 bytes ao invés de 14 (o ‘\0’ final conta também!), mas, ao usá-la em CreateWindowW você ganha tempo porque a conversão é desnecessária. E, se tiver que fazer comparações entre caracteres, não há perda de performance, já que comparar dois valores de 8 bits, 16 bits ou 32 bits gasta o mesmo tempo em processadores modernos.

O “formato de transformação” do Unicode:

Para permitir que todos os quase 1,2 milhões de caracteres possíveis possam ser usados, o Unicode define padrões de codificação conhecidos pela sigla UTF (Unicode Transformation Format). Existem 3, básicos: UTF-8, UTF-16 e UTF-32. O valor associado à sigla, é claro, representa o tamanho de cada unidade da sequência que compõe a codificação de um caracter. Assim, um caracter ‘A’ tem código 0x41 em UTF-8, 0x0041 em UTF-16 e 0x00000041 em UTF-32. Mas, como é possível usar mais que 256 caracteres com UTF-8, por exemplo? O unicode criou uma codificação para isso… Caracteres na faixa ASCII tem apenas 1 byte de tamanho e correspondência direta no UTF-8… Lembre-se que ASCII tem apenas 7 bits de tamanho. Quando o código tem mais bits a seguinte codificação é usada:

Number
of bytes
Bits for
code point
First
code point
Last
code point
Byte 1 Byte 2 Byte 3 Byte 4
1 7 U+0000 U+007F 0xxxxxxx
2 11 U+0080 U+07FF 110xxxxx 10xxxxxx
3 16 U+0800 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 21 U+10000 U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Assim, qualquer caracter pode ser expresso, tendo de 1 até 4 bytes de tamanho. Retornando ao nosso caractere de exemplo (‘𝄞’), sabendo que seu código é U+1d11e e esse valor hexadecimal é expresso, em binário (com separações usando ‘_’ para facilitar a compreensão aqui): 000_011101_000100_011110. Como o valor tem mais que 16 bits de tamanho ele só pode ser expresso, em UTF-8 com 4 bytes e dai, colocando os bits nos lugares corretos temos: 11110000 10011101 10000100 10011110, ou seja, exatamente a sequência 0xf0 0x9d 0x84 0x9e, que citei anteriormente.

A vantagem do UTF-8 está no mapeamento do ASCII, incluindo o final de string marcada com ‘\0’ de apenas 1 byte e pelo fato de que esse padrão não sobre com o problema de endiness. Em teoria os padrões de codificação que têm mais que 8 bits pode ser expressos com o LSB em primeiro lugar (Little Endian) ou com o MSB no primeiro byte (Big Endian). Assim, os padrões UTF-16, por exemplo, teria que ser subdividido em 2: UTF-16LE e UTF-16BE. Esses padrões existem, mas UTF, acima de 8 bits, também suporta uma marca chamada de BOM (Byte Order Mark). É um “caracter” que informa o endianess do caracter (ou da string)… Muito confuso, não?

Outra vantagem do UTF: Composição!

Considere isto: a⃕. Como é que eu coloquei essa setinha curvada sobre o caracter ‘a’? Seria isso um caracter único “especial”? Não… Podemos compor novas “imagens” misturando dois “caracteres”… No caso, o caracter ‘a’ e o U+20D5 (COMBINING CLOCKWISE ARROW ABOVE). A sequência pode ser obtida, em C, com a string "a\xE2\x83\x95", ou, no LINUX, para obter um caracter “especial” em UTF, basta digitar Ctrl+Shift+U e digitar o código hexadecimal 20D5, logo depois do ‘a’.

A composição não para no primeiro item… Eu poderia usar o caracter U+20E8 também (‘…’ abaixo do caracter) nesse conjunto, assim: a⃨⃕. Aqui eu digitei ‘a’, seguido de U+20d5 e U+20e8. Olha só o que acontece se eu acrescentar U+20E3:  a⃨⃕⃣. Fico esquisito, pois o glyph de U+20E3 sobrepôs os outros dois, mas compus um único caracter feito com 4!

Até onde sei, isso não é possível na implementação de UNICODE do Windows e sua página de código WINDOWS-1252.

A desvantagem de usar UTF…

UTF parece ser a solução para todos os problemas, mas tem seus problemas também… O padrão foi definido por linguistas, historiadores, tipógrafos e outros profissionais. Isso significa que certas imagens de caracteres são bastante semelhantes e, às vezes, redundantes. Considere o ‘ç’. Em unicode ele pode ser definido como U+00E7 (que tem 2 bytes) ou ‘c’ combinado com U+0327 (‘ç’, que tem 3 bytes). O caracter U+0327 é definido como “COMBINING CEDILLA” e, dependendo da fonte usada, tem a exata aparência do “cedilha” usado no ‘ç’. O mesmo pode acontecer com qualquer vogal acentuada…

Para dar outro exemplo, há alguns anos apareceu uma brincadeira que dizia para trocarmos o caracter ‘;’ (U+003B) pelo ‘;’ (U+037e) em códigos-fonte. O segundo é o ponto de interrogação “em grego”, enquanto o primeiro é o ponto-e-virgula… Mas a aparência é exatamente a mesma. Ao compilar o código o programador ficaria maluco para tentar descobrir o problema.

A capacidade de composição e a aparência idêntica de símbolos diferentes causam, obviamente, grande problema quando queremos fazer comparação com caracteres ou strings… Existem meios de mitigar esse problema, chamados de transliteração (literalmente “transcrever de uma língua para outra”), mas não é uma panaceia.

Aviso para os desenvolvedores para WEB

Já viu, alguma vez, algum site onde aparecem diversos caracteres �? Isso acontece quando você mistura charsets. Quando o desenvolvedor, especialmente para web, diz para sua IDE preferida que o HTML está codificado em UTF-8 (usando uma tag META, por exemplo) e todo o arquivo está codificado no padrão WINDOWS-1252 ou ISO-8859-1, esse caracter eventualmente aparece.

Isso também acontece com clientes de e-mail… Se meu cliente de e-mail lida com UTF-8 por padrão e você me envia uma mensagem codificada com WINDOWS-1252, mas dizendo que é ISO-8859-1, alguns caracteres especiais do WINDOWS-1252 serão mostrados como �… Lembre-se WINDOWS-1252 não é a mesma coisa que ISO-8859-1.

Anúncios