Programación en C: ¿Cómo programar para Unicode?

82

¿Qué requisitos previos se necesitan para realizar una programación Unicode estricta?

¿Esto implica que mi código no debe usar chartipos en ningún lugar y que se deben usar funciones que puedan manejar wint_ty wchar_t?

¿Y cuál es el papel que juegan las secuencias de caracteres multibyte en este escenario?

prinzdezibel
fuente

Respuestas:

21

Tenga en cuenta que no se trata de una "programación Unicode estricta" per se, sino de algo de experiencia práctica.

Lo que hicimos en mi empresa fue crear una biblioteca contenedora alrededor de la biblioteca ICU de IBM. La biblioteca contenedora tiene una interfaz UTF-8 y se convierte a UTF-16 cuando es necesario llamar a ICU. En nuestro caso, no nos preocupamos demasiado por los golpes de rendimiento. Cuando el rendimiento era un problema, también proporcionamos interfaces UTF-16 (utilizando nuestro propio tipo de datos).

Las aplicaciones pueden permanecer en gran parte como están (usando char), aunque en algunos casos deben ser conscientes de ciertos problemas. Por ejemplo, en lugar de strncpy () usamos un contenedor que evita cortar secuencias UTF-8. En nuestro caso, esto es suficiente, pero también se podrían considerar controles para combinar caracteres. También tenemos envoltorios para contar la cantidad de puntos de código, la cantidad de grafemas, etc.

Al interactuar con otros sistemas, a veces necesitamos hacer una composición de caracteres personalizada, por lo que es posible que necesite algo de flexibilidad allí (según su aplicación).

No usamos wchar_t. El uso de ICU evita problemas inesperados en la portabilidad (pero no otros problemas inesperados, por supuesto :-).

Hans van Eck
fuente
2
Una secuencia de bytes UTF-8 válida nunca sería cortada (truncada) por strncpy. Las secuencias UTF-8 válidas no pueden contener 0x00 bytes (excepto el byte nulo de terminación, por supuesto).
Dan Moulding
8
@Dan Moulding: si strncpy (), digamos, una cadena que contiene un solo carácter chino (que puede ser de 3 bytes) en una matriz de caracteres de 2 bytes, crea una secuencia UTF-8 no válida.
Hans van Eck
@Hans van Eck: si su contenedor copia ese único carácter chino de 3 bytes en una matriz de 2 bytes, entonces lo truncará y creará una secuencia no válida, o tendrá un comportamiento indefinido. Obviamente, si está copiando datos, el objetivo debe ser lo suficientemente grande; ni que decir. Mi punto fue que, si se strncpyusa correctamente, es perfectamente seguro usarlo con UTF-8.
Dan Moulding
5
@DanMoulding: si sabe que su búfer de destino es lo suficientemente grande, puede usarlo strcpy(que de hecho es seguro de usar con UTF-8). Las personas que lo usan strncpyprobablemente lo hagan porque no saben si el búfer de destino es lo suficientemente grande, por lo que quieren pasar un número máximo de bytes para copiar, lo que de hecho puede crear secuencias UTF-8 no válidas.
Frerich Raabe
41

C99 o anterior

El estándar C (C99) proporciona caracteres anchos y caracteres multibyte, pero como no hay garantía sobre lo que pueden contener esos caracteres anchos, su valor es algo limitado. Para una implementación dada, brindan soporte útil, pero si su código debe poder moverse entre implementaciones, no hay garantía suficiente de que serán útiles.

En consecuencia, el enfoque sugerido por Hans van Eck (que consiste en escribir un resumen de la biblioteca ICU - Componentes internacionales para Unicode -) es sólido, en mi opinión.

La codificación UTF-8 tiene muchos méritos, uno de los cuales es que si no se mete con los datos (truncándolos, por ejemplo), puede ser copiado por funciones que no son plenamente conscientes de las complejidades de UTF-8 codificación. Este no es categóricamente el caso de wchar_t.

Unicode en su totalidad es un formato de 21 bits. Es decir, Unicode reserva puntos de código de U + 0000 a U + 10FFFF.

Una de las cosas útiles de los formatos UTF-8, UTF-16 y UTF-32 (donde UTF significa Formato de transformación Unicode; consulte Unicode ) es que puede convertir entre las tres representaciones sin pérdida de información. Cada uno puede representar cualquier cosa que los demás puedan representar. Tanto UTF-8 como UTF-16 son formatos de varios bytes.

UTF-8 es bien conocido por ser un formato multibyte, con una estructura cuidadosa que hace posible encontrar el inicio de los caracteres en una cadena de manera confiable, comenzando en cualquier punto de la cadena. Los caracteres de un solo byte tienen el bit alto establecido en cero. Los caracteres de varios bytes tienen el primer carácter que comienza con uno de los patrones de bits 110, 1110 o 11110 (para caracteres de 2 bytes, 3 bytes o 4 bytes), y los bytes siguientes siempre comienzan con 10. Los caracteres de continuación siempre están en el rango 0x80 .. 0xBF. Existen reglas que establecen que los caracteres UTF-8 deben representarse en el formato mínimo posible. Una consecuencia de estas reglas es que los bytes 0xC0 y 0xC1 (también 0xF5..0xFF) no pueden aparecer en datos UTF-8 válidos.

 U+0000 ..   U+007F  1 byte   0xxx xxxx
 U+0080 ..   U+07FF  2 bytes  110x xxxx   10xx xxxx
 U+0800 ..   U+FFFF  3 bytes  1110 xxxx   10xx xxxx   10xx xxxx
U+10000 .. U+10FFFF  4 bytes  1111 0xxx   10xx xxxx   10xx xxxx   10xx xxxx

Originalmente, se esperaba que Unicode fuera un conjunto de códigos de 16 bits y que todo encajara en un espacio de código de 16 bits. Desafortunadamente, el mundo real es más complejo y tuvo que expandirse a la codificación actual de 21 bits.

Por tanto, UTF-16 es un conjunto de códigos de una sola unidad (palabra de 16 bits) para el 'Plano multilingüe básico', es decir, los caracteres con puntos de código Unicode U + 0000 .. U + FFFF, pero utiliza dos unidades (32 bits) para caracteres fuera de este rango. Por lo tanto, el código que funciona con la codificación UTF-16 debe poder manejar codificaciones de ancho variable, al igual que UTF-8. Los códigos para los caracteres de doble unidad se denominan sustitutos.

Los sustitutos son puntos de código de dos rangos especiales de valores Unicode, reservados para su uso como valores iniciales y finales de unidades de código emparejadas en UTF-16. Los sustitutos principales, también llamados altos, son de U + D800 a U + DBFF, y los sustitutos finales o bajos son de U + DC00 a U + DFFF. Se les llama sustitutos, ya que no representan personajes directamente, sino solo como pareja.

UTF-32, por supuesto, puede codificar cualquier punto de código Unicode en una sola unidad de almacenamiento. Es eficiente para la computación pero no para el almacenamiento.

Puede encontrar mucha más información en los sitios web de ICU y Unicode.

C11 y <uchar.h>

El estándar C11 cambió las reglas, pero no todas las implementaciones se han puesto al día con los cambios incluso ahora (mediados de 2017). El estándar C11 resume los cambios para el soporte Unicode como:

  • Caracteres y cadenas Unicode ( <uchar.h>) (originalmente especificado en ISO / IEC TR 19769: 2004)

Lo que sigue es un resumen mínimo de la funcionalidad. La especificación incluye:

6.4.3 Nombres de caracteres universales

Sintaxis
nombre-carácter-universal:
    \u hex-quad
    \U hex-quad hex-quad
hex-quad:
    dígito-hexadecimal dígito-hexadecimal dígito-hexadecimal dígito-hexadecimal

7.28 utilidades Unicode <uchar.h>

El encabezado <uchar.h>declara tipos y funciones para manipular caracteres Unicode.

Los tipos declarados son mbstate_t(descritos en 7.29.1) y size_t(descritos en 7.19);

char16_t

que es un tipo entero sin signo utilizado para caracteres de 16 bits y es del mismo tipo que uint_least16_t(descrito en 7.20.1.2); y

char32_t

que es un tipo entero sin signo utilizado para caracteres de 32 bits y es del mismo tipo que uint_least32_t(también descrito en 7.20.1.2).

(Traduciendo las referencias cruzadas: <stddef.h>define size_t, <wchar.h>define mbstate_ty <stdint.h>define uint_least16_ty uint_least32_t). El <uchar.h>encabezado también define un conjunto mínimo de funciones de conversión (reiniciables):

  • mbrtoc16()
  • c16rtomb()
  • mbrtoc32()
  • c32rtomb()

Existen reglas sobre qué caracteres Unicode se pueden usar en identificadores usando notaciones \unnnno \U00nnnnnn. Es posible que deba activar activamente el soporte para dichos caracteres en identificadores. Por ejemplo, GCC requiere -fextended-identifierspermitir estos en identificadores.

Tenga en cuenta que macOS Sierra (10.12.5), por nombrar solo una plataforma, no es compatible <uchar.h>.

Jonathan Leffler
fuente
3
Creo que estás vendiendo wchar_ty amigos un poco cortos aquí. Estos tipos son esenciales para permitir que la biblioteca C maneje texto en cualquier codificación (incluidas las codificaciones que no son Unicode). Sin los amplios tipos de caracteres y funciones, la biblioteca C requeriría un conjunto de funciones de manejo de texto para cada codificación admitida: imagine tener koi8len, koi8tok, koi8printf solo para texto codificado en KOI-8 y utf8len, utf8tok, utf8printf para UTF-8 texto. En cambio, tenemos la suerte de tener un conjunto de estas funciones (sin contar los originales ASCII): wcslen, wcstok, y wprintf.
Dan Moulding
1
Todo lo que un programador debe hacer es usar las funciones de conversión de caracteres de la biblioteca C ( mbstowcsy amigos) para convertir cualquier codificación compatible a wchar_t. Una vez en wchar_tformato, el programador puede utilizar el conjunto único de funciones de manejo de texto amplio que proporciona la biblioteca C. Una buena implementación de la biblioteca C admitirá prácticamente cualquier codificación que la mayoría de los programadores necesitarán (en uno de mis sistemas, tengo acceso a 221 codificaciones únicas).
Dan Moulding
En cuanto a si serán lo suficientemente anchos para ser útiles: el estándar requiere una implementación que debe garantizar que wchar_tsea ​​lo suficientemente amplia para contener cualquier carácter soportado por la implementación. Esto significa (con posiblemente una excepción notable) que la mayoría de las implementaciones se asegurarán de que sean lo suficientemente amplias como para que un programa que las utilice wchar_tmaneje cualquier codificación admitida por el sistema (la de Microsoft wchar_ttiene solo 16 bits de ancho, lo que significa que su implementación no es totalmente compatible con todas las codificaciones, más notablemente las diversas codificaciones UTF, pero la suya es la excepción, no la regla).
Dan Moulding
11

Estas preguntas frecuentes son una gran cantidad de información. Entre esa página y este artículo de Joel Spolsky , tendrá un buen comienzo.

Una conclusión a la que llegué en el camino:

  • wchar_tes de 16 bits en Windows, pero no necesariamente de 16 bits en otras plataformas. Creo que es un mal necesario en Windows, pero probablemente se pueda evitar en otros lugares. La razón por la que es importante en Windows es que necesita usar archivos que tienen caracteres que no son ASCII en el nombre (junto con la versión W de las funciones).

  • Tenga en cuenta que las API de Windows que toman wchar_tcadenas esperan codificación UTF-16. Tenga en cuenta también que esto es diferente a UCS-2. Tome nota de los pares sustitutos. Esta página de prueba tiene pruebas esclarecedoras.

  • Si usted es la programación en Windows, no se puede utilizar fopen(), fread(), fwrite(), etc, ya que sólo tienenchar * y no entienden codificación UTF-8. Hace que la portabilidad sea dolorosa.

dbyron
fuente
Tenga en cuenta que stdio f*y sus amigos trabajan con char *en todas las plataformas porque el estándar lo dice: use wcs*en su lugar para wchar_t.
gato
7

Para realizar una programación Unicode estricta:

  • Utilice únicamente cadenas de API que son conscientes Unicode ( NO strlen , strcpy, ... pero sus homólogos WideStringwstrlen , wsstrcpy, ...)
  • Cuando se trata de un bloque de texto, utilice una codificación que permita almacenar caracteres Unicode (utf-7, utf-8, utf-16, ucs-2, ...) sin pérdida.
  • Compruebe que el juego de caracteres predeterminado de su sistema operativo sea compatible con Unicode (por ejemplo, utf-8)
  • Utilice fuentes que sean compatibles con Unicode (por ejemplo, arial_unicode)

Las secuencias de caracteres de varios bytes es una codificación anterior a la codificación UTF-16 (la que se usa normalmente con wchar_t ) y me parece que es más bien solo para Windows.

Nunca he oído hablar de eso wint_t.

sebastien
fuente
wint_t es un tipo definido en <wchar.h>, al igual que wchar_t. Tiene el mismo papel con respecto a los caracteres anchos que int tiene con respecto a 'char'; puede contener cualquier valor de carácter amplio o WEOF.
Jonathan Leffler
3

Lo más importante es siempre hacer una clara distinción entre texto y datos binarios . Trate de seguir el modelo de Python 3.x strvs.bytes o SQL TEXTvs BLOB.

Desafortunadamente, C confunde el problema al usar chartanto "carácter ASCII" como int_least8_t. Querrás hacer algo como:

typedef char UTF8; // for code units of UTF-8 strings
typedef unsigned char BYTE; // for binary data

Es posible que también desee typedefs para unidades de código UTF-16 y UTF-32, pero esto es más complicado porque la codificación de wchar_tno está definida. Necesitará solo un preprocesador #if. Algunas macros útiles en C y C ++ 0x son:

  • __STDC_UTF_16__- Si está definido, el tipo _Char16_texiste y es UTF-16.
  • __STDC_UTF_32__- Si está definido, el tipo _Char32_texiste y es UTF-32.
  • __STDC_ISO_10646__- Si está definido, entonces wchar_tes UTF-32.
  • _WIN32- En Windows, wchar_tes UTF-16, aunque rompe el estándar.
  • WCHAR_MAX- Se puede usar para determinar el tamaño de wchar_t, pero no si el sistema operativo lo usa para representar Unicode.

¿Esto implica que mi código no debería usar tipos de caracteres en ningún lugar y que se deben usar funciones que puedan manejar wint_t y wchar_t?

Ver también:

No. UTF-8 es una codificación Unicode perfectamente válida que usa char*cadenas. Tiene la ventaja de que si su programa es transparente a bytes que no son ASCII (por ejemplo, un convertidor de final de línea que actúa \ry \npasa a través de otros caracteres sin cambios), ¡no necesitará hacer ningún cambio!

Si va con UTF-8, deberá cambiar todas las suposiciones que char= carácter (por ejemplo, no llamar toupperen un bucle) o char= columna de pantalla (por ejemplo, para ajustar texto).

Si elige UTF-32, tendrá la simplicidad de los caracteres de ancho fijo (pero no los grafemas de ancho fijo , pero deberá cambiar el tipo de todas sus cadenas).

Si elige UTF-16, tendrá que descartar tanto la suposición de caracteres de ancho fijo como la suposición de unidades de código de 8 bits, lo que hace que esta sea la ruta de actualización más difícil de las codificaciones de un solo byte.

Recomendaría evitarlo activamente wchar_tporque no es multiplataforma: a veces es UTF-32, a veces es UTF-16 y, a veces, es una codificación de Asia oriental pre-Unicode. Recomendaría usartypedefs

Aún más importante, eviteTCHAR .

dan04
fuente
No creo que sea desafortunado en absoluto, que el char sea un int. Eso es un beneficio. El uso de constantes de caracteres literales viene a la mente como un uso. Y las funciones que toman una char *pueden tener problemas si se pasan una const char *última que recuerdo (pero soy vago sobre esto y qué funciones, así que tómalo con una pizca de sal). El hecho de que sea más complicado con otros lenguajes no significa que sea un mal diseño.
Pryftan
2

No confiaría en ninguna implementación de biblioteca estándar. Simplemente use sus propios tipos Unicode.

#include <windows.h>

typedef unsigned char utf8_t;
typedef unsigned short utf16_t;
typedef unsigned long utf32_t;

int main ( int argc, char *argv[] )
{
  int msgBoxId;
  utf16_t lpText[] = { 0x03B1, 0x0009, 0x03B2, 0x0009, 0x03B3, 0x0009, 0x03B4, 0x0000 };
  utf16_t lpCaption[] = L"Greek Characters";
  unsigned int uType = MB_OK;
  msgBoxId = MessageBoxW( NULL, lpText, lpCaption, uType );
  return 0;
}

fuente
2

Básicamente, desea tratar las cadenas en la memoria como wchar_tmatrices en lugar de char. Cuando realiza cualquier tipo de E / S (como leer / escribir archivos), puede codificar / decodificar usando UTF-8 (esta es probablemente la codificación más común) que es lo suficientemente simple de implementar. Solo busca en Google las RFC. Entonces, en la memoria, nada debe ser de varios bytes. Unowchar_t representa un personaje. Sin embargo, cuando se trata de serializar, es cuando necesita codificar en algo como UTF-8, donde algunos caracteres están representados por varios bytes.

También tendrá que escribir nuevas versiones de strcmpetc. para las cadenas de caracteres anchas, pero esto no es un gran problema. El mayor problema será la interoperabilidad con bibliotecas / código existente que solo aceptan matrices de caracteres.

Y cuando se trata de sizeof(wchar_t)(necesitará 4 bytes si quiere hacerlo bien) siempre puede redefinirlo a un tamaño más grande con typedef/ macrohacks si lo necesita.

Mike Weller
fuente
1

Por lo que sé, wchar_t depende de la implementación (como se puede ver en este artículo de wiki ). Y no es Unicode.

PolyThinker
fuente