UTF-8, wchar_t ou outro formato?

Meu grande amigo Marz me apresentou um problema interessante: Qual codificação de charset devemos usar? Como vocês podem ver neste e neste artigos, na minha opinião, UTF-8 é o crème de la crème. Eu acho isso porque:

  • UTF-8 é uma codificação multibyte, mas os caracteres ASCII são diretamente mapeados no UTF-8, e em apenas um byte;
  • A variedade de caracteres que podem ser codificados em UTF-8 é enorme;
  • É possível “misturar” caracteres.

Claro que UTF-8 tem suas complicações. Por exemplo: Um único caracter pode ocupar até 5 bytes na string. Outro problema é que uma “mistura” pode, em teoria, ser feita de algumas maneiras diferentes…

Só que eu acho que do ponto-de-vista do desenvolvimento, esses problemas não são tão complexos assim. Mesmo em charsets singlebyte como o Latin-1, ou ISO-8859-1, a comparação de sequências de caracteres é feita byte-a-byte. Assim, “Shrodinger” é diferente de “Shrödinger”, se você usar a função strcmp para compará-las. A solução para este problema parece ser a transformação de ambas as strings sendo comparadas para seus equivalentes sem acentuações e depois a comparação.

Essa aproximação enfrenta o problema do multibyte do UTF-8. Então, temos que transformar nossas strings para outro formato que não use caracteres multibyte. Uma maneira é transformá-las numa condificação widechar. Felizmente a biblioca libc fornece as funções de conversão de charsets da libiconv. Olha só um exemplo (simples):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iconv.h>

void dump8(char *p);
void dump16(wchar_t *p);

int main(int argc, char **argv)
{
  iconv_t cd;
  size_t src_len, dest_len, sz;
  wchar_t *d, *deststr;

  /* SIM! Podemos fazer isso! hehe */
  char *srcstr = "مثليون جنسيا مثل لترجمة النصوص العربية";

  setlocale(LC_ALL, "");

  if ((cd = iconv_open("WCHAR_T", "UTF-8")) == (iconv_t)-1)
  {
    fprintf(stderr, "Erro ao inicializar contexto do iconv.\n");
    abort();
  }

  /* É seguro determinar o tamanho do buffer
     destino como tendo o mesmo número de caracteres
     do buffer original, já que o destino
     não é multibyte, só Widechar! */
  src_len = strlen(srcstr);
  dest_len = sizeof(wchar_t) * src_len;

  d = deststr =
    (wchar_t *)malloc(dest_len + sizeof(wchar_t));

  /* Inclui o '' no final! */
  printf("UTF-8 string dump (%d bytes):\n",
    src_len+sizeof(char));
  dump8(srcstr);
  printf("\n\"%s\"\n", srcstr);

  /* ATENÇÂO: iconv() altera os ponteiros! */
  sz = iconv(cd, &amp;srcstr, &amp;src_len, &amp;d, &amp;dest_len);

  if (sz != -1)
  {
    *d = 0;

    sz = 2 * (wcslen(deststr)+1);

    /* Inclui o \x0000 no final */
    printf("\nwchar_t string dump (%d bytes):\n", sz);

    dump16(deststr);
    printf("\n\"%ls\"\n", deststr);
  }
  else
  {
    fprintf(stderr, "Error converting string.\n");
    abort();
  }

  free(deststr);

  iconv_close(cd);

  return 0;
}

void dump8(char *p)
{
  int count = 0;

  while (*p)
  {
    printf("%02X ", (unsigned char)*p);
    if (( ++count % 8 ) == 0)
      printf("\n");
    p++;
  }
}

void dump16(wchar_t *p)
{
  int count = 0;

  while (*p)
  {
    printf("%04X ", (unsigned short)*p);
    if (( ++count % 8 ) == 0)
      printf("\n");
    p++;
  }
}

Ao executar o programa temos:

$ ./test
UTF-8 string dump (72 bytes):
D9 85 D8 AB D9 84 D9 8A
D9 88 D9 86 20 D8 AC D9
86 D8 B3 D9 8A D8 A7 20
D9 85 D8 AB D9 84 20 D9
84 D8 AA D8 B1 D8 AC D9
85 D8 A9 20 D8 A7 D9 84
D9 86 D8 B5 D9 88 D8 B5
20 D8 A7 D9 84 D8 B9 D8
B1 D8 A8 D9 8A D8 A9
"مثليون جنسيا مثل لترجمة النصوص العربية"

wchar_t string dump (78 bytes):
0645 062B 0644 064A 0648 0646 0020 062C
0646 0633 064A 0627 0020 0645 062B 0644
0020 0644 062A 0631 062C 0645 0629 0020
0627 0644 0646 0635 0648 0635 0020 0627
0644 0639 0631 0628 064A 0629
"مثليون جنسيا مثل لترجمة النصوص العربية"

Ok, por que foi que eu chamei esse exemplo de “simples”? É que cada caractere, que não seja o espaço, está codificado, em UTF-8, em exatamente 2 bytes. Dai há quase uma correspondência direta entre cada um dos caracteres da string original e da string destino (widechar). A diferença é que, na string destino, TODOS os caracteres tem 2 bytes de tamanho, incluindo os espaços (e, por isso, a string destino é um pouco maior que a original).

Assim, de posse da string WdeChar, você pode comparar caractere por caractere sem se preocupar com os multibytes do UTF-8.

Mas – e sempre existe um “mas”! – temos um problema: Nem todos os caracteres UTF-8 podem ser traduzidos diretamente para wchar_t. E, também, a implementação de iconv() na libc não é tão boa quanto a da libiconv.

Na libiconv você pode criar um descritor assim:

iconv_t ic = iconv_open("WCHAR_T//TRANSLIT", "UTF-8");

Esse “//TRANSLIT” ai permite que, se um caracter não for “traduzível” para o charset destino, então libiconv fará o melhor possível para encontrar um caractere mais próximo. Isso tem o potencial de minimizar o problema das “misturas” que podemos fazer no UTF-8.

Só que, se tentar usar isso na rotina iconv_open(), que acompanha a libc, vai tomar um SIGSEGV na cara quando tentar usar a rotina iconv().

Artigo originalmente postado no antigo Lost in the e-Jungle.

Um comentário sobre “UTF-8, wchar_t ou outro formato?

Deixe um comentário