domingo, 6 de junio de 2010

Usando Literales String en Unicode en C++

Me metí en un problema, pensé que podría tratar el tema brevemente, pero no, no es así...

Después que leí un poco me convencí de lo miserablemente ignorante que soy. Todo este cuento de los encodings para las cadenas de caracteres (strings), de ASCII con sus code pages, de los Wide Characters, de los multibyte characters, de unicode con sus UTF-8, UTF-16, UTF-32, de los Locales, del UCS (Universal Caracter Set) con sus UCS-2 y UCS-4. ¡Que sopa de letras!. En el continente americano y buena parte de Europa, por lo general no vamos mucho más allá de manejar los caracteres ASCII Latin-1 (ISO-8859-1) o el Windows-1252.

En fin me pregunté, ¿como manejar los nuevos encodings en Linux con C++?, si, me refiero a los encodings del estándard Unicode: UTF-8, UTF-16, UTF-32, no piensen que los voy a explicar aquí, para eso pongo las referencias, además los aburriría.

Al día de hoy Linux soporta UTF-8 de forma nativa, es por ello que hacer un programa en C++ que trabaje con este encoding es lo más directo que hay. Para demostrar que estoy usando Unicode usaré junto con los clásicos caracteres ASCII, caracteres griegos, cirílicos y una simpática flecha que pondré como ejemplo de una referencia a un Code Point.

Empecemos por declarar un string clásico de lenguaje C (const char*) usando UTF-8 dentro de nuestro código fuente:

Archivo: unicode.cpp:
const char *str_utf8 = "Hola mundo Unicode UTF-8: Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";

A manera de ejemplo puse unos code points de Unicode (para que los excépticos vean que sí estamos usando unicode). Esta cadena es de unos 64 caracteres de longitud (ojo dije caracteres, no dije bytes). En el nuevo estándar de C++, el prefijo "u8" se usa para indicar que el encoding de una cadena es UTF-8, "u" (minúscula) para cadenas UTF-16 y "U" (mayúscula) para UTF-32. Sin embargo, requeriría al menos de la versión 4.5.x del compilador de GNU para hacer uso de todos ellos, así que mantengamos el ejercicio simple: si tu ambiente linux soporta UTF-8, estos ejemplos deberían funcionar.

El primer experimento que haremos sera este:

#include <iostream>
#include <cstring>
#include <string>

using namespace std;

const char *str_utf8 = "Hola mundo Unicode UTF-8: Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";

int main(int argc, char **argv) {
cout << "String clásico C: " << endl
<< " " << str_utf8 << endl
<< " longitud según strlen: " << strlen(str_utf8) << endl;

string cpp_string(str_utf8);
cout << "String de la STL de C++:" << endl
<< " " << cpp_string << endl
<< " longitud según strlen: " << cpp_string.length() << endl;

return 0;
}


Deberán compilar con la linea de comando:

%> g++ unicode.cpp -o unicode

Si tienen los fonts correctamente instalados en su sistema, podrán ver los caracteres griegos, cirílicos y demás al ejecutar este programa en consola, de lo contrario les recomiendo los instalen (después de todo leen esto porque Unicode les ha dado curiosidad). Si ya leyeron de UTF-8 se podrán explicar al ejecutar esta línea, por qué la rutina de C para determinar la longitud de un string no sirve. strlen cuenta bytes y una cadena UTF-8 consta de caracteres "multi-bytes", un solo caracter puede tener entre 1 y 4 bytes. strlen reporta entonces una longitud que está por encima de la correcta y la clase std::string de librería STL de C++ aun no resuelve este problema.

Conociendo un poco de UTF-8, no es dificil deducir una rutina para contar los caracteres. Cada caracter compatible con ASCII es de la forma binaria "0xxxxxxx". Cada caracter multibyte empieza con la forma binaria "11xxxxxx" y los subsiguientes bytes que le corresponden son de la forma binaria "10xxxxxx", de modo que sólo nos interesan estos dos bits iniciales. Podemos hacer entonces un "and" binario (&) para decidir si contamos el byte como un caracter o no:

int utf8_strlen(const char *s) {
int j=0;
for (int i=0; s[i]; ++i)
if ((s[i] & 0xC0) != 0x80) j++;
return j;
}


Recordar que el hexadecimal 0xC0 es en binario "11000000", mientras que 0x80 es "10000000".

Nos encontramos nuevamente en el escenario de siempre... vamos a querer hacer una clase StringUTF8. Les pido que la busquen antes de hacerla.

Por otro lado algunos diríamos, usemos un std::wstring:

#include <iostream>
#include <cwchar>
#include <string>

using namespace std;

const wchar_t *wstr = L"Hola mundo Unicode UTF-8: Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";

int main(int argc, char **argv) {
wstring cpp_wstring(wstr);
wcout << "String de la STL de C++:" << endl
<< " " << cpp_wstring << endl
<< " longitud según strlen: " << cpp_wstring.length() << endl;

return 0;
}



Esto resuelve el problema de la longitud que es contada correctamente. Sin embargo el despliegue en consola del string no es correcto. No se pueden visualizar los caracteres pues la consola es UTF-8. Este encoding con wide-chars, caracteres anchos, se corresponde con el estandard UCS-4, que equivale a UTF-32. En este caso cada caracter es de 4 bytes, por ello es facil contar su longitud.

El último de los encondings que queda es el UTF-16. Sólo el nuevo estándar de C++ permite representar literales string en UTF-16 directamente en el código fuente de un programa (con el prefijo "u" frente a un string). De hecho, aprovecho esta sección para indicar cómo sería la declaración de los literales UTF en cada caso:

// UTF-8: prefijo "u8"
const char *str_utf_8 = u8"Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";
// UTF-16: prefijo "u" (minúscula)
const char16_t *str_utf_16 = u"Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";
// UTF-32: prefijo "U" (mayúscula)
const char32_t *str_utf_32 = U"Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";


Para que el compilador de GNU reconozca esos literales, tendrían que compilar su código con la opción para el nuevo estándar:
g++ -std=c++0x unicode.cpp -o unicode

Para las versiones del compilador que aun no soportan el nuevo estándar, se pueden declarar cadenas UTF-16. Recuerden a Java, una tecnología construida en C/C++, siempre ha ofrecido un soporte nativo a UTF-16. Veamos cómo se define en JNI (Java Native Interface) un caracter:

typedef unsigned short jchar;

Dado que no tenemos un soporte nativo para la declaración de un literal UTF-16, la única forma que quedaría sería entonces mediante un arreglo de jchar's inicializado:

#include <iostream>

using namespace std;

typedef unsigned short jchar;

const jchar jstr[] = {'H','o','l','a',' ','m','u','n','d','o',' ','U','n','i','c','o','d','e',' ','U','T','F','-','8',':',' ',
'G','r','i','e','g','o','s',' ',0x279c,' ',0x3a3,' ',0x3a5,' ',0x398,' ',0x3a9,'.',' ',
'C','i','r',0xED,'l','i','c','o','s',' ',0x279c,' ',0x402,' ',0x409,' ',0x40a,' ',0x416, 0};

int main(int argc, char **argv) {
int i;
for (i=0; jstr[i] & 0x00FF; ++i)
cout << static_cast<char> (jstr[i]);
cout << endl << "i:" << i << endl;

return 0;
}

Parece obvio por qué JNI usa funciones para convertir strings de Java a strings de C/C++.

Un último punto que quiero tocar, también muy superficialmente, es el tema de escribir estos literales en archivos. Lo primero que noté es que el locale de C/C++ afecta la forma en la que el literal se escribe en disco. Como el tema de los Locales es como para escribir otro artículo, sólo usaré el locale por defecto, en este escenario el wide string se convierte a UTF-8 cuando se utiliza el wofstream de C++, las cadenas UTF-8 se escriben sin problemas. Para obligar la escritura cruda de wide strings UTF-32 y de las cadenas UTF-16, se debe recurrir a las primitivas básicas de lenguaje C:

#include <iostream>
#include <string>
#include <cstdio>
#include <fstream>

using namespace std;

typedef unsigned short jchar;

const wchar_t *wstr = L"Hola mundo Unicode UTF-8: Griegos \u279c Σ Φ Θ Ω. Cirílicos \u279c Ђ Љ Њ Ж";
const jchar jstr[] = {'H','o','l','a',' ','m','u','n','d','o',' ','U','n','i','c','o','d','e',' ','U','T','F','-','8',':',' ',
'G','r','i','e','g','o','s',' ',0x279c,' ',0x3a3,' ',0x3a5,' ',0x398,' ',0x3a9,'.',' ',
'C','i','r',0xED,'l','i','c','o','s',' ',0x279c,' ',0x402,' ',0x409,' ',0x40a,' ',0x416, 0};

int main(int argc, char **argv) {
locale::global( locale( "" ) ) ;

wofstream ofile("test_utf_8.utf", ios_base::out | ios_base::trunc);
if (ofile.bad()) {
cout << "No se pudo abrir el archivo\n";
}
else {
ofile << wstr;
ofile.close();
}

wstring cpp_str(wstr);
FILE *cf_output = fopen("test_utf32.utf", "w");
fwrite(reinterpret_cast<const void*> (wstr), sizeof(wchar_t), cpp_str.length(), cf_output);
fclose(cf_output);

cf_output = fopen("test_utf_16.utf", "w");
fwrite(reinterpret_cast<const void*> (jstr), sizeof(jchar), cpp_str.length(), cf_output);
fclose(cf_output);

return 0;
}


Para verificar el tipo de estos archivos generados, lo pueden hacer con un editor hexadecimal como Bless o un editor unicode como EditPad Lite. Para este último, necesitarán Wine para poder ejecutarlo en Linux, pues es una aplicacion Win32.

Referencias

No hay comentarios:

Publicar un comentario

Seguidores