web-dev-qa-db-fra.com

Programmation C: Comment programmer pour Unicode?

Quelles conditions préalables sont nécessaires pour effectuer une programmation Unicode stricte?

Cela implique-t-il que mon code ne doit pas utiliser de types char n'importe où et que des fonctions doivent être utilisées pour traiter wint_t et wchar_t?

Et quel est le rôle joué par les séquences de caractères multi-octets dans ce scénario?

80
prinzdezibel

Notez qu'il ne s'agit pas de "programmation unicode stricte" en soi, mais d'une expérience pratique.

Ce que nous avons fait dans mon entreprise était de créer une bibliothèque d'encapsulage autour de la bibliothèque IBM ICU. La bibliothèque d'encapsuleur a une interface UTF-8 et se convertit en UTF-16 lorsqu'il est nécessaire d'appeler ICU. Dans notre cas, nous ne nous sommes pas trop inquiétés des problèmes de performances. Lorsque les performances étaient un problème, nous avons également fourni des interfaces UTF-16 (en utilisant notre propre type de données).

Les applications peuvent rester en grande partie telles quelles (en utilisant char), bien que dans certains cas, elles doivent être conscientes de certains problèmes. Par exemple, au lieu de strncpy (), nous utilisons un wrapper qui évite de couper les séquences UTF-8. Dans notre cas, cela suffit, mais on pourrait également envisager des vérifications pour combiner des caractères. Nous avons également des wrappers pour compter le nombre de points de code, le nombre de graphèmes, etc.

Lors de l'interfaçage avec d'autres systèmes, nous devons parfois faire une composition de caractères personnalisée, vous pouvez donc y avoir besoin d'une certaine flexibilité (en fonction de votre application).

Nous n'utilisons pas wchar_t. L'utilisation de ICU évite les problèmes inattendus de portabilité (mais pas d'autres problèmes inattendus, bien sûr :-).

21
Hans van Eck

C99 ou version antérieure

La norme C (C99) prévoit des caractères larges et des caractères multi-octets, mais comme il n'y a aucune garantie quant à ce que ces caractères larges peuvent contenir, leur valeur est quelque peu limitée. Pour une implémentation donnée, elles fournissent un support utile, mais si votre code doit pouvoir se déplacer entre les implémentations, il n'y a pas de garantie suffisante qu'elles seront utiles.

Par conséquent, l'approche suggérée par Hans van Eck (qui consiste à écrire un wrapper autour de la bibliothèque ICU - International Components for Unicode)) est saine, IMO.

L'encodage UTF-8 a de nombreux avantages, dont l'un est que si vous ne salissez pas avec les données (en les tronquant, par exemple), il peut être copié par des fonctions qui ne sont pas pleinement conscientes des subtilités de l'UTF-8. codage. Ce n'est absolument pas le cas avec wchar_t.

Unicode dans son intégralité est un format 21 bits. Autrement dit, Unicode réserve les points de code de U + 0000 à U + 10FFFF.

L'une des choses utiles sur les formats UTF-8, UTF-16 et UTF-32 (où UTF signifie Format de transformation Unicode - voir nicode ) est que vous pouvez convertir entre les trois représentations sans perte de information. Chacun peut représenter tout ce que les autres peuvent représenter. UTF-8 et UTF-16 sont des formats multi-octets.

UTF-8 est bien connu pour être un format multi-octets, avec une structure soignée qui permet de trouver le début des caractères dans une chaîne de manière fiable, en commençant à n'importe quel point de la chaîne. Les caractères à un octet ont le bit haut mis à zéro. Les caractères multi-octets ont le premier caractère commençant par l'un des modèles de bits 110, 1110 ou 11110 (pour les caractères 2 octets, 3 octets ou 4 octets), les octets suivants commençant toujours 10. Les caractères de continuation sont toujours dans le plage 0x80 .. 0xBF. Il existe des règles selon lesquelles les caractères UTF-8 doivent être représentés dans le format minimum possible. Une conséquence de ces règles est que les octets 0xC0 et 0xC1 (également 0xF5..0xFF) ne peuvent pas apparaître dans les données UTF-8 valides.

 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

À l'origine, on espérait qu'Unicode serait un ensemble de codes 16 bits et que tout irait dans un espace de code 16 bits. Malheureusement, le monde réel est plus complexe et a dû être étendu à l'encodage 21 bits actuel.

UTF-16 est donc un ensemble de codes à une seule unité (mot 16 bits) pour le "plan multilingue de base", ce qui signifie les caractères avec les points de code Unicode U + 0000 .. U + FFFF, mais utilise deux unités (32 bits) pour caractères en dehors de cette plage. Ainsi, le code qui fonctionne avec le codage UTF-16 doit être capable de gérer des codages à largeur variable, tout comme le doit UTF-8. Les codes des caractères à double unité sont appelés substituts.

Les substituts sont des points de code provenant de deux plages spéciales de valeurs Unicode, réservées pour une utilisation en tant que valeurs de début et de fin d'unités de code appariées en UTF-16. Les substituts de tête, également appelés hauts, sont de U + D800 à U + DBFF, et les substituts de fin ou bas, de U + DC00 à U + DFFF. Ils sont appelés substituts, car ils ne représentent pas directement les personnages, mais uniquement sous forme de paire.

L'UTF-32, bien sûr, peut coder n'importe quel point de code Unicode dans une seule unité de stockage. Il est efficace pour le calcul mais pas pour le stockage.

Vous pouvez trouver beaucoup plus d'informations sur les sites Web ICU et Unicode.

C11 et <uchar.h>

La norme C11 a changé les règles, mais toutes les implémentations n'ont pas encore rattrapé les changements (mi-2017). La norme C11 résume les modifications de la prise en charge Unicode comme suit:

  • Caractères et chaînes Unicode (<uchar.h>) (Initialement spécifiés dans ISO/IEC TR 19769: 2004)

Ce qui suit est un aperçu minimal de la fonctionnalité. La spécification comprend:

6.4.3 Noms de caractères universels

Syntaxe
nom-caractère-universel:
\u hex-quad
\U hex-quad hex-quad
hex-quad:
chiffre hexadécimal chiffre hexadécimal chiffre hexadécimal chiffre hexadécimal

7.28 Utilitaires Unicode <uchar.h>

L'en-tête <uchar.h> Déclare les types et les fonctions de manipulation des caractères Unicode.

Les types déclarés sont mbstate_t (Décrit au 7.29.1) et size_t (Décrit au 7.19);

char16_t

qui est un type entier non signé utilisé pour les caractères 16 bits et est du même type que uint_least16_t (décrit au 7.20.1.2); et

char32_t

qui est un type entier non signé utilisé pour les caractères 32 bits et qui est du même type que uint_least32_t (également décrit au 7.20.1.2).

(Traduction des références croisées: <stddef.h> Définit size_t, <wchar.h> Définit mbstate_t Et <stdint.h> Définit uint_least16_t Et uint_least32_t.) L'en-tête <uchar.h> Définit également un ensemble minimal de fonctions de conversion (pouvant être redémarrées):

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

Il existe des règles concernant les caractères Unicode qui peuvent être utilisés dans les identificateurs à l'aide des notations \unnnn Ou \U00nnnnnn. Vous devrez peut-être activer activement la prise en charge de ces caractères dans les identifiants. Par exemple, GCC requiert -fextended-identifiers Pour les autoriser dans les identifiants.

Notez que macOS Sierra (10.12.5), pour ne nommer qu'une plate-forme, ne prend pas en charge <uchar.h>.

37
Jonathan Leffler

Cette FAQ est une mine d'informations. Entre cette page et cet article de Joel Spolsky , vous aurez un bon départ.

Une conclusion à laquelle je suis parvenu en cours de route:

  • wchar_t Est 16 bits sur Windows, mais pas nécessairement 16 bits sur d'autres plates-formes. Je pense que c'est un mal nécessaire sur Windows, mais peut probablement être évité ailleurs. La raison pour laquelle il est important sous Windows est que vous en avez besoin pour utiliser des fichiers qui ont des caractères non ASCII dans le nom (avec la version W des fonctions).

  • Notez que les API Windows qui prennent des chaînes wchar_t Attendent le codage UTF-16. Notez également que ceci est différent de UCS-2. Prenez note des paires de substitution. Cette page de test a des tests éclairants.

  • Si vous programmez sous Windows, vous ne pouvez pas utiliser fopen(), fread(), fwrite(), etc. car ils ne prennent que char * Et ne comprennent pas le codage UTF-8. Rend la portabilité douloureuse.

10
dbyron

Pour effectuer une programmation Unicode stricte:

  • Utilisez uniquement des API de chaîne compatibles Unicode ( ET NON strlen, strcpy, ... mais leurs homologues à chaînes étendues wstrlen, wsstrcpy, ...)
  • Lorsque vous traitez un bloc de texte, utilisez un encodage qui permet de stocker les caractères Unicode (utf-7, utf-8, utf-16, ucs-2, ...) sans perte.
  • Vérifiez que le jeu de caractères par défaut de votre système d'exploitation est compatible Unicode (ex: utf-8)
  • Utilisez des polices compatibles Unicode (par exemple, arial_unicode)

Les séquences de caractères multi-octets sont un codage qui précède le codage UTF-16 (celui utilisé normalement avec wchar_t) et il me semble que c'est plutôt Windows uniquement.

Je n'ai jamais entendu parler de wint_t.

7
sebastien

La chose la plus importante est de toujours faire une distinction claire entre le texte et les données binaires . Essayez de suivre le modèle Python 3.x str vs bytes ou SQL TEXT vs BLOB.

Malheureusement, C confond le problème en utilisant char pour "caractère ASCII" et int_least8_t. Vous voudrez faire quelque chose comme:

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

Vous voudrez peut-être aussi des typedefs pour les unités de code UTF-16 et UTF-32, mais cela est plus compliqué car le codage de wchar_t n'est pas défini. Vous aurez juste besoin d'un préprocesseur #ifs. Certaines macros utiles en C et C++ 0x sont:

  • __STDC_UTF_16__ - S'il est défini, le type _Char16_t existe et est UTF-16.
  • __STDC_UTF_32__ - S'il est défini, le type _Char32_t existe et est UTF-32.
  • __STDC_ISO_10646__ - Si défini, alors wchar_t est UTF-32.
  • _WIN32 - Sous Windows, wchar_t est UTF-16, même si cela brise la norme.
  • WCHAR_MAX - Peut être utilisé pour déterminer la taille de wchar_t, mais pas si le système d'exploitation l'utilise pour représenter Unicode.

Cela implique-t-il que mon code ne doit utiliser aucun type de char n'importe où et que des fonctions doivent être utilisées pour gérer wint_t et wchar_t?

Voir également:

Non. UTF-8 est un encodage Unicode parfaitement valide qui utilise char* chaînes. Il a l'avantage que si votre programme est transparent pour les octets non ASCII (par exemple, un convertisseur de fin de ligne qui agit sur \r et \n mais passe à travers les autres caractères inchangés), vous ne devrez faire aucun changement!

Si vous optez pour UTF-8, vous devrez modifier toutes les hypothèses selon lesquelles char = caractère (par exemple, n'appelez pas toupper dans une boucle) ou char = colonne d'écran (par exemple, pour l'habillage du texte).

Si vous optez pour UTF-32, vous aurez la simplicité des caractères à largeur fixe (mais pas à largeur fixe graphemes, mais vous devrez changer le type de toutes vos chaînes).

Si vous optez pour UTF-16, vous devrez ignorer l'hypothèse de caractères à largeur fixe et l'hypothèse d'unités de code 8 bits, ce qui en fait le chemin de mise à niveau le plus difficile à partir de codages à un octet.

Je recommanderais activement d'éviter wchar_t parce que ce n'est pas multiplateforme: Parfois c'est UTF-32, parfois c'est UTF-16, et parfois c'est un encodage est-asiatique pré-Unicode. Je recommanderais d'utiliser typedefs

Plus important encore, éviter TCHAR .

3
dan04

Je ne ferais confiance à aucune implémentation de bibliothèque standard. Lancez simplement vos propres types 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;
}
2
user2074102

Vous voulez essentiellement traiter les chaînes en mémoire en tant que tableaux wchar_t au lieu de char. Lorsque vous effectuez tout type d'E/S (comme la lecture/l'écriture de fichiers), vous pouvez encoder/décoder en utilisant UTF-8 (c'est probablement l'encodage le plus courant) qui est assez simple à implémenter. Recherchez simplement les RFC sur Google. Donc, en mémoire, rien ne devrait être multi-octets. Un wchar_t représente un caractère. Cependant, lorsque vous arrivez à la sérialisation, vous devez encoder quelque chose comme UTF-8 où certains caractères sont représentés par plusieurs octets.

Vous devrez également écrire de nouvelles versions de strcmp, etc. pour les chaînes de caractères larges, mais ce n'est pas un gros problème. Le plus gros problème sera l'interopérabilité avec les bibliothèques/le code existant qui n'acceptent que les tableaux de caractères.

Et quand il s'agit de sizeof (wchar_t) (vous aurez besoin de 4 octets si vous voulez le faire correctement), vous pouvez toujours le redéfinir à une plus grande taille avec typedef/macro hacks si vous en avez besoin.

2
Mike Weller

D'après ce que je sais, wchar_t dépend de l'implémentation (comme on peut le voir --- article wiki ). Et ce n'est pas unicode.

1
PolyThinker