Convertir de ASCIIZ à Unicode (et inversement) avec .NET

Convertir de ASCIIZ à Unicode (et inversement) avec .NET

Mon illustre collègue François Jean me souligne que le Framework 2.0 procède différemment de la version 1.0 décrite ici. Je constate (avec plaisir!) que la prochaine version de Visual Studio (nom de code Orcas) utilisera aussi des techniques plus proches des usages C++ contemporaine (approche marshal_as, proche du lexical_cast). Quand Microsoft aura (enfin!) stabilisé son implémentation, j'ajusterai ce texte en conséquence.

L'un des gestes les plus malheureux de la démarche .NET est d'avoir nommé un langage C++ .NET, ce qui entraîne la confusion chez les gens qui pensent qu'il s'agit de C++ avec un bonus alors qu'il s'agit en réalité d'un C++ édulcoré et amoindri (pas de vraies constantes, pas de vrais destructeurs, pas d'héritage multiple, etc.) qui existe comme citoyen de seconde zone dans le monde .NET.

On peut faire pire, mais C++ .NET n'est ni le C++ puissant du monde ISO, ni le citoyen de première classe de .NET qu'est C#. Et surtout, c'est un citoyen duquel on s'attendrait à ce qu'il fasse le pont entre les deux mondes, mais qui au contraire vit mal dans les deux mondes.

L'un des problèmes que rencontrent souvent les gens qui veulent profiter des bibliothèques graphiques de .NET (car après tout, l'une des principales raisons pour utiliser un produit comme C++ .NET est d'avoir recours à des outils standardisés pour les IPM graphiques, car le langage C++ ISO ne propose pas de bibliothèques standards pour les IPM graphiques) avec C++ .NET est le traitement du texte – la manipulation de chaînes de caractères.

En effet, dans le monde .NET, les chaînes de caractères se manipulent à partir du type String (avec un grand « S »), qui est un alias pour System.String (ou System::String avec C++ .NET). Dans le monde C++, les chaînes de caractères standards sont représentées par la classe std::string (avec un petit « s ») qui correspond au template (type générique) std::basic_string<> appliqué au type char.

Les deux ne sont pas compatibles, et ne se situent pas au même niveau de généricité; de même, le type std::string manipule des séquences ASCIIZ de char (caractères encodés sur huit bits), alors que le type .NET qu'est System::String manipule des séquences de caractères Unicode (16 bits).

Les deux seraient plus proches l'un de l'autre si on utilisait des std::wstring (qui équivaut à std::basic_string<> appliqué au type wchar_t) en C++, mais nous laisserons ce cas en exercice à la lectrice et au lecteur.

Si on veut exploiter du code C++ standard et y mêler les bibliothèques .NET, il faut souvent trouver moyen de faire coexister ces deux entités que sont std::string et System::String. Heureusement, on peut passer par des structures plus primitives pour naviguer avec un confort relatif sans trop se casser la tête.

Après tout, System::String représente une sorte d'enrobage autour d'une séquence de caractèrs Unicode (des wchar_t ), tout comme std::wstring, alors que std::string représente une séquence ASCIIZ. Si on comprend cela, et si on tient compte que les deux classes exposent des constructeurs capables de créer un objet à partir de données primitives, alors nous sommes prêts à procéder.

L'extrait de programme suivant prend la chaîne .NET nommée sNet (manipulée à travers un pointeur, ce qui est la seule manière de manipuler des objets .NET), en dépose le contenu dans une std::string, puis affiche cette std::string avec std::cout.

System::String *sNet = new System::String("allo .NET");

// Connaître le nombre de caractères dans la chaîne .NET
const int TAILLE_CHAINE_NET = sNet->get_Length();

// Copier les caractères de la chaîne .NET originale à la chaîne
// standard (l'opérateur += insère à la fin de la chaîne). Si nous
// avions utilisé une std::wstring, la conversion explicite de type
// (le static_cast<>) aurait été évitée.
std::string sStd;
for (int i = 0; i < TAILLE_CHAINE_NET; i++)
   sStd += static_cast<char>(sNet->get_Chars(i));

// Bingo!
std::cout << sStd << std::endl;

Remarquez la conversion explicite de types (static_cast<>), utilisée pour convertir chaque caractère Unicode (chaque wchar_t) en caractère ASCII (en char). Si on examine le même programme mais utilisant std::wstring et std::wcout pour maintenir la version Unicode des caractères, alors on obtient:

System::String *sNet = new System::String ("allo .NET");

// Connaître le nombre de caractères dans la chaîne .NET
const int TAILLE_CHAINE_NET = sNet->get_Length ();

// Copier les caractères de la chaîne .NET originale à la chaîne
// standard (l'opérateur += insère à la fin de la chaîne). Si nous
// avions utilisé une std::wstring, la conversion explicite de type
// (le static_cast<>) aurait été évitée.
std::wstring sStd;
for (int i = 0; i < TAILLE_CHAINE_NET; i++)
   sStd += sNet->get_Chars (i);

// Bingo!
std::wcout << sStd << std::endl;

Dans l'ordre inverse, donc de chaîne standard à chaîne .NET, c'est encore plus simple du fait que les chaînes standard (les std::string et les std::wstring) offrent toutes deux la méthode c_str() qui expose une séquence ASCIIZ constante sur le texte représenté dans la chaîne. Cette séquence peut simplement être passée au constructeur de la chaîne .NET (de l'instance de System::String), et le tour est joué.

std::string sStd = "allo standard";

// Il existe un constructeur de System::String qui prend en paramètre
// une séquence ASCIIZ (la même chose existe pour une séquence de
// wchar_t), et std::string offre une méthode qui expose une telle
// séquence. Pas beaucoup de travail à faire ici.
System::String *sNet = new System::String(sStd.c_str ());

// Bingo!
Console::WriteLine(sNet);

Avec une std::wstring, ça donne exactement le même résultat (même si le constructeur appelé n'est pas le même) :

std::wstring sStd = L"allo standard";

System::String *sNet = new System::String(sStd.c_str ());

// Re-bingo!
Console::WriteLine(sNet);

Valid XHTML 1.0 Transitional

CSS Valide !