Qt et les chaînes de caractères : UTF-8, Latin1 et charsets

| Thiago Macieira | Traducteur : Thibaut Cuvelier |

La dernière fois, nous avons arrêté nos expérimentations sur la faisabilité de remplacer le codec Latin1 par défaut par UTF-8. Le texte prévu était plus long, mais a été raccourci pour se concentrer sur le code et les performances, sans tenir compte du contexte, des raisons, des détails.

Commentez

- Titre : Qt et les chaînes de caractères : UTF-8, Latin1 et charsets

- Auteurs : Thiago Macieira, Traducteur : Thibaut Cuvelier

- Parution : 2 avril 2011

- Mise à jour : 20 avril 2011

- Licence : Copyright ® 2011 Thiago Macieira. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. L'article original

- II. Contexte : les charsets mandatés par le standard C++

- III. QString aujourd'hui

- IV. Entrée dans C++11 avec une solution (partielle) : les littéraux Unicode

- V. Conclusion

- VI. Remerciements

I - L'article original

Le site Qt Labs permet aux développeurs de Qt de présenter les projets, plus ou moins avancés, sur lesquels ils travaillent.

Nokia, Qt, Qt Quarterly et leurs logos sont des marques déposées de Nokia Corporation en Finlande et/ou dans les autres pays. Les autres marques déposées sont détenues par leurs propriétaires respectifs.

Cet article est la traduction de l'article On UTF-8, Latin 1 and charsets de Thiago Macieira paru dans Qt Labs.

II - Contexte : les charsets mandatés par le standard C++

Les standards C et C++ parlent de deux charsets : celui d'entrée des sources et celui de l'exécution. Le manuel de GCC en dénombre quatre, en y ajoutant le charset de caractères larges (de nos jours, UTF-16 ou UCS-4, en fonction de la largeur des caractères voulue) et le charset utilisé en interne par le compilateur. Ici, nous allons nous limiter aux deux premiers.

Le charset d'entrée des sources est celui dans lequel vos fichiers source sont encodés. Aux débuts du C, quand les charsets étaient très différents, comme EBCDIC, il était très important qu'il soit bien configuré, sinon le compilateur ne comprenait pas quels octets représentaient un espace ou un retour à la ligne. Aujourd'hui, on peut écrire un compilateur qui se base sur le fait que le charset d'entrée est l'ASCII et l'utiliser sans problème. Ce charset est utilisé par le compilateur quand il charge le fichier en mémoire et le traduit dans une forme qu'il peut analyser syntaxiquement.

Le charset d'exécution est celui dans lequel vos chaînes sont encodées quand le compilateur écrit les fichiers objet. Si vous écrivez un mot importé en anglais comme Résumé, le compilateur doit trouver un moyen d'encoder ces é. Notons que le compilateur a chargé le fichier source en mémoire et l'a converti en un certain format interne avant compilation ; on se basera donc ici sur le fait que le compilateur a compris qu'il s'agissait de caractères LATIN SMALL LETTER E WITH ACUTE. La manière dont ces é étaient sur le disque n'a rien à voir avec la manière dont cet article est encodé.

Le manuel de GCC dit que le paramètre par défaut pour le charset d'entrée était le charset de la locale ; pour l'exécution, UTF-8. Ce n'est pas exactement vrai : à moins de le spécifier précisément, GCC ressortira les mêmes octets qu'à l'entrée. On peut le vérifier en tentant de compiler un fichier encodé en Latin1 avec une locale dont l'encodage est UTF-8. Comme attendu, cela fonctionne. Je suppose que changer ce comportement casserait trop de programmes, c'est pourquoi les développeurs de GCC ne l'ont pas fait. Mais, si on ajoute une option -finput-charset= ou -fexec-charset=, même aux paramètres que l'on supposait par défaut, GCC va afficher des erreurs s'il trouve quelque chose qui n'est pas correctement encodé.

Il y a une ou deux semaines, il y a eu une discussion à ce sujet sur le channel IRC #qt de Freenode. Un développeur voulait savoir pourquoi QString utilisait Latin1 au lieu du charset d'exécution pour décoder les chaînes littérales. Il voulait aussi savoir pourquoi il ne pouvait pas simplement écrire \u00fc pour signifier le caractère ü. La réponse est simple et double :

1. Qt et QString ne connaissent pas le charset d'exécution choisi lors de la compilation des sources ;

2. le charset d'exécution n'est pas constant : un fichier objet peut avoir un charset différent d'un autre objet ou d'une bibliothèque.

III - QString aujourd'hui

En regardant comment fonctionne QString aujourd'hui, on peut voir qu'il y a un certain support pour un charset d'exécution variable. Quand on dit qu'il vaut par défaut Latin1, cela signifie implicitement qu'il peut être changé. Dans la documentation de QString, toute fonction qui peut prendre un const char * fait référence à la fonction QString::fromAscii(). En fait, cette fonction est mal nommée : elle ne convertit pas nécessairement depuis de l'ASCII - la documentation précise que en fonction du codec, elle peut ne pas accepter des entrées valides US-ASCII (ANSI X3.4-1986).

Cette fonction est nommée fromAscii parce que la majorité du code actuel est écrit en ASCII. Cette fonction a été introduite dans Qt3 et a été publiée en 2002. À cette époque, UTF-8 n'était pas aussi répandu qu'actuellement. Cela signifiait que tout fichier avec des octets non ASCII allait probablement être mal interprété à l'envoi à quelqu'un dans le monde, mais nettement moins probablement qu'à un collègue dans le même pays.

Ainsi, de petites équipes développant des applications voulaient parfois utiliser ces caractères non ASCII, déjà listés (°, €...). Pour les héberger, QString autorise le changement du codec qu'il utilise pour décoder les chaînes littérales.

En d'autres termes, QTextCodec::setCodecForCStrings permet de dire à Qt quel est le charset d'exécution, voici la solution au premier problème. Par contre, pour le second, il n'y a rien qui puisse aider, les bibliothèques devront continuer à dire à Qt quel codec elles utilisent pour leurs chaînes.

IV - Entrée dans C++11 avec une solution (partielle) : les littéraux Unicode

Le prochain standard C++ contient une nouvelle manière d'écrire des chaînes, qui permet de s'assurer qu'elles sont toujours encodées dans un charset UTF : les nouvelles chaînes littérales. On peut donc écrire du code comme ceci :

u8"Je suis une chaîne UTF-8."
u"Voici une chaîne UTF-16."
U"Voici une chaîne UTF-32."

Du côté récepteur, QString saura quel encodage est UTF-8, UTF-16 ou UTF-32, sans le moindre doute possible. En fait, presque : les chaînes encodées en UTF-8 finissent en const char[], ce qui n'est pas très différent des chaînes littérales actuelles. Ainsi, Qt ne peut pas les distinguer. Les deux autres, cependant, génèrent de nouveaux types, respectivement const char16_t[] et const char32_t[], que l'on peut utiliser dans des surcharges et décoder parfaitement.

Ainsi, le développeur sur IRC pourrait écrire sans crainte u"\u00fc" et être sûr que QString le décodera en tant que LATIN SMALL LETTER U WITH DIAERESIS (U+00FC).

La critique que l'on peut adresser au comité C++ est qu'ils n'ont résolu le problème que partiellement. On veut écrire u"Résumé" et envoyer le fichier à un collègue utilisant une autre plateforme (comme Windows). De plus, on veut que son compilateur interprète le code source comme on le désire. Évidemment, cela signifie que l'on va encoder le fichier source en UTF-8, on aimerait donc que tout compilateur utilise UTF-8 comme charset d'entrée.

Le comité C++ n'en a rien dit, n'a même pas donné un moyen de marquer les fichiers d'une telle manière. Le décodage des fichiers source dépend vraiment des paramètres du compilateur...

V - Conclusion

Si on ne peut pas dire au compilateur dans quel charset sont encodés les fichiers source, il faudra trouver une manière efficace de créer des QString. En interne, QString stocke les données en UTF-16 et cela ne va pas changer. Ainsi, on doit avoir le compilateur à convertir le code source en UTF-16. En utilisant les nouvelles chaînes littérales UTF-16, on peut le faire. Puisque ces chaînes sont en mémoire en lecture seule, on ne peut pas les décharger, on peut même faire :

QString s = QString::fromRawData(u"Résumé");

Le compilateur pouvant mal interpréter les é, on devrait peut-être faire ceci :

QString s = QString::fromRawData(u"R\u00e9sum\u00e9");

Il s'agit néanmoins d'une solution assez verbeuse. Quelqu'un suggérait d'utiliser des macros pour ce faire. Mais, si on utilise une autre fonctionnalité du C++11, les chaînes littérales définies par l'utilisateur, on pourrait définir l'opérateur suivant :

QString operator "" q(const char16_t *str, size_t len);

On pourrait donc écrire la ligne suivante, un peu bizarre mais au moins propre :

QString s = u"Résumé"q;

Malheureusement, la dernière version de GCC ne l'a toujours pas implémenté...

VI - Remerciements

Merci à Louis du Verdier et à Claude Leloup pour leur relecture !