Code du cours ne fonctionne pas sur mon ordi... Je suis coinced

mbstowcs (convertion de caractères larges)

a marqué ce sujet comme résolu.

Salut !

J’essaye de tester un code du cours sur mon ordi, il ne fonctionne pas. Il s’agit d’entrer une chaine de caractères dans le terminal, de mettre celle-ci dans une chaine de caractères larges, puis de la re convertir en chaine de chars, et de la réafficher dans le terminal (exercice de cours quoi).

Dans le cours, cela correspond à cette partie : Notions avancées -> Les caractères larges -> Les entrées récupérées depuis le terminal -> La fonction wcstombs (lien : https://zestedesavoir.com/tutoriels/755/le-langage-c-1/notions-avancees/les-caracteres-larges/#la-fonction-wcstombs)

Je vous copie bêtement le code d’exemple du cours que j’ai exécuté :

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
	char ligne[255];

	if (fgets(ligne, sizeof ligne, stdin) == NULL)
	{
		perror("fgets");
		return EXIT_FAILURE;
	}
	if (setlocale(LC_CTYPE, "") == NULL)
	{
		perror("setlocale");
		return EXIT_FAILURE;
	}

	wchar_t ws[sizeof ligne];

	if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
	{
		perror("mbstowcs");
		return EXIT_FAILURE;
	}

	size_t n = wcstombs(ligne, ws, sizeof ligne);

	if (n == (size_t)-1)
	{
		perror("wcstombs");
		return EXIT_FAILURE;
	}

	printf("%zu multiplet(s) écrit(s) : %s\n", n, ligne);
	return 0;
}

Voici ce qu’il produit dans l’exemple du cours :

Élégant
10 multiplet(s) écrit(s) : Élégant

… Et voici ce que cela produit sur mon ordi :

élégant
8 multiplet(s) écrit(s) : 'l'gant

Bien-sûr, la question porte sur le 'é' de "élégant".

Voilà si vous avez des pistes/des idées… Merci beaucoup :'(

(je suis sous windows 11)

+0 -0

Je pense qu’il y a un problème d’encodage, à cause du message écrit(s). Quoi qu’il en soit, que se passe-t il si tu rentre Élégant ?

+0 -0

Bien, je t’invite à lire ce tutoriel: Comprendre l’encodage.

Ici c’est un cas typique de chaîne UTF-8 interprétée en latin-1.

C’est que manifestement ton terminal n’utilise pas l’encodage de ton compilateur ^^"

De plus, la raison pour laquelle tu obtiens est que Windows Terminal (par défaut sur Windows 11) ne supporte pas les entrées en UTF-8. Il les remplaces par 0 manifestement.

Le ticket de bug: https://github.com/microsoft/terminal/issues/11956

+2 -0

Oui l’encodage du compilateur et celui de la console sont différents, c’est justement ce que l’appel à setlocale est sensé résoudre

AScriabine

pas tout à fait, setlocale permet d’utiliser la locale du système (Windows) mais le terminal est un programme comme un autre, alors comme ton petit programme, il décide quel encodage il utilise pour faire son rendu (et même sa saisie) qui n’est pas forcément celui du système. (ce que je trouve bizarre, je n’ai pas vraiment la réponse au poste initial de pourquoi ça marche pas, j’essaie juste d’éclaircir un point)

Remarque par ailleurs que le "é" de "écrit" ne produit pas le même affichage que le "é" de "élégant". Cela parce que "écrit" vient du litéral encodé par le format de ton fichier (dans quel encodage as-tu enregistré ton fichier ?) et "élégant" vient de la saisie utilisateur encodé dans le format de travail du Terminal.

+1 -0

Oui l’encodage du compilateur et celui de la console sont différents, c’est justement ce que l’appel à setlocale est sensé résoudre d’après le cours…

Ah donc tu es au courant que c’est ça le problème de écrit => écrit. J’avais mal compris ta mise en situation.

Du coup pourquoi setlocale ne fonctionne pas comme attendu, ce serait plutôt ça la question ?

Alors là, je réponds que justement, le Windows Terminal ne permet pas d’entré en UTF-8. D’après, le bug trouvé sur Github.

Si tu as python d’installer tu peux éventuellement tester ceci pour voir si le problème est bien le même.

>>> s = os.read(0, 10)
こんにちわ
>>> s

Ou pour être plus Français (🐓 🇫🇷 🥖) car le japonais c’est pas simple à taper pour nous :

>>> s = os.read(0, 8)
élégant
>>> s

Si tu remplaces:


	char ligne[255];

	if (fgets(ligne, sizeof ligne, stdin) == NULL)
	{
		perror("fgets");
		return EXIT_FAILURE;
	}

Par :

char ligne[255] = "éléphant !";

Ça marche mieux ?

+1 -0

Le systeme Windows est assez vicieux en ce qui concerne les modes de conversion.

Voici mon petit test en Python:

>>> ord("é")                                                                                                            
233                                                                                                                     
>>> chr(233)                                                                                                            
'é'                                                                                                                     
>>> hex(233)                                                                                                            
'0xe9'                                                                                                                  
>>> import os                                                                                                           
>>> os.system("chcp")                                                                                                   
Page de codes active : 850                                                                                              
0                                                                                                                       
>>> os.system("chcp 65001")                                                                                             
Page de codes active : 65001                                                                                            
0                                                                                                                       
>>> ord("é")                                                                                                            
233                                                                                                                     
>>>                                                                                                                      

Le code 65001 correspond au UTF-8.

Tout se passe comme si on ramenait tout en Latin-1 …

Je n’étais pas satisfait de mon test. Alors j’ai essayé ceci:

#include <stdio.h>
#include <string.h>
int main(void) {
    char string[20];
    fgets(string, 20, stdin);
    printf("'%s'\n", string);
   printf("l=%lld\n", (size_t)strlen(string));   // Je compile avec -Wall et -Wextra
}

Ce qui donne ceci en passant du code850 à 65001

>ac
clés                                                                                                                    
'clés                                                                                                                   
'                                                                                                                       
l=5                                                                                                                     
                                                                                                                       
>chcp 65001                                                                    
Page de codes active : 65001                                                                                            
                                                                                                                        
>ac                                                                            
clés                                                                                                                    
'cl'                                                                                                                    
l=2                                                                                                                     

Donc le 'é' est converti en 0.

+1 -0

Salut,

Certains éléments ont déjà été donnés, mais pour reprendre le tout, il faut distinguer deux choses :

  • La chaîne littérale "%zu multiplet(s) écrit(s) : %s\n" dont l’encodage dépend du compilateur (sauf à la préfixer avec u8 auquel cas ce sera de l’UTF-8). Le plus souvent ce sera de l’UTF-8 (c’est ce que font GCC et Clang par exemple). L’affichage dans le terminal est incorrect, parce que ton terminal n’attend pas de l’UTF-8. À vue de nez c’est du Windows-1252 (qui est identique au latin-1), en tous les cas ce qui est affiché correspond bien aux caractères avec les points de code 0xC3 (Ã) et 0xA9 (©), qui sont les deux bytes de é en UTF-8. Si tu utilises GCC, tu peux forcer l’usage de l’encodage Windows-1252 pour les chaînes littérales avec l’option -fexec-charset=windows-1252.

  • La chaîne récupérée via le terminal qui est d’abord convertie vers une chaîne de caractères larges à l’aide de la fonction mbstowcs(), puis convertie vers une chaîne de caractères à l’aide de la fonction wcstombs(). Vu que ton terminal utilise l’encodage Windows-1252, le nombre de bytes est correct : en Windows-1252 é est sur un byte et non deux. Par contre le résultat n’est pas bon et je ne vois pas pourquoi de prime abord… Est-ce que tu sais me donner le résultat du code ci-dessous pour la même entrée ?

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void
print_hex(char *s, size_t size) {
	for (size_t i = 0; i < size; i++) {
		printf("%02x ", (unsigned char)s[i]);
	}

	printf("\n");
}


int
main(void) {
	char ligne[255];

	if (fgets(ligne, sizeof ligne, stdin) == NULL) {
		perror("fgets");
		return EXIT_FAILURE;
	}

	printf("Contenu de ligne : ");
	print_hex(ligne, strlen(ligne));
	char *locale = setlocale(LC_CTYPE, "");

	if (locale == NULL) {
		perror("setlocale");
		return EXIT_FAILURE;
	}

	printf("Locale actuelle : %s.\n", locale);
	wchar_t ws[sizeof ligne];
	size_t n = mbstowcs(ws, ligne, sizeof ligne);

	if (n == (size_t)-1) {
		perror("mbstowcs");
		return EXIT_FAILURE;
	}

	printf("mbstowcs a écrit %zu caractère(s) large(s).\n", n);
	printf("Contenu de ws : ");
	print_hex((char *)ws, n * sizeof(wchar_t));
	n = wcstombs(ligne, ws, sizeof ligne);

	if (n == (size_t)-1) {
		perror("wcstombs");
		return EXIT_FAILURE;
	}

	printf("wcstombs a écrit %zu caractère(s).\n", n);
	printf("Contenu de ligne : ");
	print_hex(ligne, n);
	return 0;
}

Cela donne ceci chez moi, sous du Linux.

élégant
Contenu de ligne : c3 a9 6c c3 a9 67 61 6e 74 0a 
Locale actuelle : fr_BE.UTF-8.
mbstowcs a écrit 8 caractère(s) large(s).
Contenu de ws : e9 00 00 00 6c 00 00 00 e9 00 00 00 67 00 00 00 61 00 00 00 6e 00 00 00 74 00 00 00 0a 00 00 00 
wcstombs a écrit 10 caractère(s).
Contenu de ligne : c3 a9 6c c3 a9 67 61 6e 74 0a
+1 -0

Salut,

Les jeux de caractères est un problème qu’un débutant devrait contourner, c’est loin d’être simple.

Tout d’abord on doit distinguer:

  1. le jeu de caractère utilisé quand on édite le code source. Par exemple: si j’écris dans le code char x[] = "à"; l’éditeur va écrire dans le fichier texte un octet qui correspond au 'à' (mais peut-être plusieurs sont nécessaires p.e. en utf-8.)
  2. le jeu de caractère utilisé par défaut dans les chaines de caractères du code. Correspond à l’encodage de tout ce qui était dans le code sous la forme '.' ou "..." sans préfixe. par exemple: "à" est devenu une séquence d’octets terminée par un 0 qui dépend de l'execution-charset alors que u8"à" est garanti en utf-8.
  3. le jeu de caractère utilisé par la console (ou si on écrit/lit un fichier, celui du fichier.)

Sous Linux, on peut compter sur le fait que les 3 sont utf-8. Donc on ne devrait pas avoir de problème particulier. Sauf que en utf-8 dès les caractères accentués, un caractère nécessite plusieurs char, et ça peut perturber un débutant.

Sous Windows, pourquoi faire simple quand on peut faire compliqué:

  • Le jeu de caractère utilisé dans les fichiers texte est très souvent windows-1252 en Europe.
  • Le jeu de caractère du code est variable. Par exemple Visual avait windows-1252 jusqu’à Visual2019, et serait en utf-8 depuis Visual2022.
  • La console Windows est par défaut en IBM-850 en Europe. On peut changer la page de code mais il semblerait que les modes UTF8 et UTF16 soient bancales.

Tenter d’écrire un code qui s’adapte à ça dans tous les cas, est possible (du moins je le pense, mais ça ne fait que 40 ans que je fais du C et je ne m’y risquerais pas!) Il faut donc essayer de se ramener à un cas plus simple:

  • n’utiliser que des caractères ASCII
  • ou passer sous Linux
  • ou, on peut forcer tout le monde à s’entendre pour avoir le même charset. Je pense que le plus simple avec Visual: supposer qu’il retrouve seul le charset du fichier source, utiliser l’option /fexec-charset=windows-1252 et envoyer vers la console au début du programme la ligne de code system("chcp 1252 > NULL"); comme ça tout le monde est en 1252.

Tu as dû remarquer que je n’ai même pas cité le setlocale() du cours. Son rôle consiste à gérer certaines choses dont l’encodage des caractères du code lors d’échanges, c’est dans la partie que je propose de contourner. restons simple ;)

PS: j’ai utilisé plein de vocabulaires pour "jeu de caractères", "charset", "page de code", "encodage"; dis-toi que ça désigne la même chose.

  • La chaîne récupérée via le terminal qui est d’abord convertie vers une chaîne de caractères larges à l’aide de la fonction mbstowcs(), puis convertie vers une chaîne de caractères à l’aide de la fonction wcstombs(). Vu que ton terminal utilise l’encodage Windows-1252, le nombre de bytes est correct : en Windows-1252 é est sur un byte et non deux. Par contre le résultat n’est pas bon et je ne vois pas pourquoi de prime abord… Est-ce que tu sais me donner le résultat du code ci-dessous pour la même entrée ?
Taurre

Après quelques essais, j’ai en partie la réponse à ma question.

Avant de continuer, quelques éléments, parce que ce que je dis est probablement pas forcément clair :

  • Le type wchar_t doit être capable de représenter tous les caractères de toutes les localisations supportées (ceci induit donc presque toujours plus qu’un byte puisque les localisations asiatiques sont supportées et ont trop de caractères pour pouvoir tout représenter avec 255 valeurs possibles).
  • Comme pour les chaînes de caractères littérales, l’encodage des chaînes de caractères larges littérales (préfixées par L) est déterminé par le compilateur. Il s’agit par exemple de l’UTF-32 sous Linux et de l’UTF-16 sous Windows. Il peut être modifié via l’option -fwide-exec-charset de GCC.
  • Lors de l’exécution du programme (donc hors chaînes littérales), les encodages utilisé lors d’une conversion entre caractères et caractères larges sont déterminés par la localisation. Dans le cas de la localisation C, celle par défaut, les caractères sont supposés avoir une valeur sur un byte (dit autrement, si on tente de convertir une chaîne encodée en UTF-8 avec la localisation C, chaque byte va être considéré comme représentant un caractère).

Ceci étant dit, reprenons le tout :

  • Comme l’as préciser @dalfab, par défaut la console Windows utilise l’encodage IBM 850. Quand tu entres élégant dans le terminal, tu reçois donc la suite \x82\x6c\x82\x67\x61\x6e\x74 (tu as aussi un caractère de fin ligne \n, mais peu importe pour la suite).
  • L’appel à setlocale() modifie les encodages utilisés pour les conversions entre caractères et caractères larges. En l’occurrence, avec une localisation francophone sous Windows, les caractères larges seront encodés en UTF-16 et les caractères en Windows-1252.
  • L’appel à mbstowcs() va convertir la chaîne élégant en une chaînes de caractères larges. Vu l’appel à setlocale() elle considère que cette chaîne est encodée en Windows-1252 (et non en IMB 850). Tu obtiens alors ceci en UTF-16 : \x20\x1a\x6c\x00\x20\x1a\x67\x00\x61\x00\x6e\x00\x74\x00. En Windows-1252, le point de code 0x82 (qui est celui du é en IMB 850) correspond au caractère (portant le doux nom de « Guillemet-Virgule inférieur ») qui, en Unicode, a le point de code 0x201a.
  • L’appel à wcstombs() va convertir la chaîne de caractères larges précédentes en une chaîne de caractères. On obtient donc à nouveau \x82\x6c\x82\x67\x61\x6e\x74.
  • Finalement on affiche la chaîne initiale et là, ça foire et je sais pas pourquoi. En fait, dès l’appel à setlocale(), l’affichage de la chaîne initialement lue ne fonctionne plus alors que si on récupère une autre chaîne en entrée, elle sera encodée de la même manière. J’ignore ce que fais setlocale() sous Windows dans ce cas ci, mais l’affichage n’est plus correct. En revanche les conversions entre caractères et caractères larges sont tout à fait correctes (si on omet le fait que l’encodage des caractères n’est pas celui utilisé par le terminal).

La seule solution pour que cela fonctionne est, comme le propose @dalfab, c’est de faire appel à chcp 1252.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte