Journal Vous avez dit "caractère" ?

Posté par  (site web personnel, Mastodon) . Licence CC By‑SA.
21
4
sept.
2022

Ces derniers jours, j'ai cherché à mieux comprendre comment gérer UTF-8 dans une de mes applications hobby et j'ai appris pas mal de choses :)

D'abord, j'avais oublié que ASCII était codé sur 7 bits et non pas 8 bits. C'est grâce à ça que UTF-8 est automatiquement compatible avec ASCII (UTF-8 est codé avec des blocs de 8-bits, il leur a suffit de dire que le premier bit est 0 pour les 127 premiers Unicodes encodé en UTF-8).

Les 7 bits m'ont surpris, car ce n'est pas dans nos habituelles puissances de 2. Mais en fait, ASCII fait partie d'une époque où la mémoire et les disques étaient vraiment très cher et il valait donc vraiment mieux ne pas être trop gourmand.

J'ai également appris que pour le langage C, le type char n'est pas forcément dédié aux caractères, mais plutôt à un stockage d'au moins 8 bits1.

En pratique, les tailles minimum pour les types C, sont: char avec au moins 8 bits (1 octet), short et integer 16 bits, long 32 bits et long long 64 bits.

Donc, pour pouvoir stocker des entiers sur (au moins) 8 bits (et non pas sur au moins 16 bits), il faut utiliser char. C'est pour stocker ces entiers que l'on a aussi signed char et unsigned char, même si ça n'a pas de sens d'avoir un "caractère signé" a priori :)

Ensuite, j'ai enfin trouvé à quoi sert GString dans GLib et pourquoi c'est toujours dit "compatible UTF-8" partout dans la documentation des fonctions liées à GString: d'après sa description2, il faut juste interpréter une GString comme un tableau dynamique de bytes avec la sûreté d'avoir le caractère NUL de terminaison de string et d'avoir une propriété len qui donne le nombre d'octet jusqu'à ce caractère NUL. En plus ce type est associés à plusieurs fonctions généralistes de gestion de texte.

Et là, je me suis dit, mais en fait ça ressemble énormément à std::string de C++: un tableau dynamique d'octets avec une propriété len. Mais si je me souviens bien, il y a d'autres types de string en C++ pour la gestion Unicode ? À quoi servent-ils ?

Eh bien, il y a effectivement std::wstring et 3 autres. wstring utilise le type wchar_t qui est un "wide character", mais qui n'est de nouveau pas définit explicitement dans le standard C.

En cherchant (encore !) des explications sur StackOverflow à propos de wchar, j'ai trouvé ce lien des "personnes qui sont contre leur utilisation": https://utf8everywhere.org/

Ils donnent beaucoup d'information à propos d'UTF-8 vs UTF-16 vs UTF-32 et pourquoi ils pensent que c'était inutile d'inventer wchar_t que finalement seul std::string était utile au C++3.

En reprenant son pendant GString en C, j'ai enfin eu la confirmation du pourquoi c'est effectivement suffisant pour stocker des strings en UTF-8, puisqu'il faut juste pouvoir avoir un ByteArray pour le stockage.

Pour l’interprétation de la donnée, il faut évidemment utiliser les bonnes fonctions (comme utiliser g_uf8_normalize avant de faire des comparaisons de string, par exemple) et bien comprendre quelle définition de "caractère" on a en tête (utf8everywhere donne 7 définitions différentes et incompatibles !).

Voilà, maintenant pour mon application en C, je sais que j'ai besoin de pouvoir traverser un mot donné par itération sur ses "grapheme cluster". Il ne me reste plus qu'à trouver un bon moyen de le faire :)


  1. le standard n'est pas explicite sur la taille des types de base. char doit pouvoir contenir le basic execution character set et garantir que sa valeur numérique est non-négative. Ce qui revient en pratique à utiliser au moins 8 bits pour char

  2. oui, j'ai donné le lien de l'ancienne documentation, parce que la nouvelle a perdu cette description. J'ai essayé de rapporter le bug, en espérant l'avoir ouvert dans le bon projet ! 

  3. à la condition que le standard dise que le basic execution character set doit être capable de stocker n'importe quelle donnée Unicode. Ce serait facile de le faire grâce à UTF-8 qui est compatible avec la définition actuelle de char

  • # + archéologie

    Posté par  (site web personnel, Mastodon) . Évalué à 6.

    Pour compléter sur la partie historique, j'étais tombé sur ces discussions intéressantes

    Pour en revenir au sujet, j'avais lu un billet de Giovanni Dicanio que j'avais un peu hésité à publier en lien ici : How Many Strings Does C++ Have? avec des commentaires intéressants sur cette jungle.

    “It is seldom that liberty of any kind is lost all at once.” ― David Hume

  • # Hum...

    Posté par  (site web personnel) . Évalué à 5.

    Les 7 bits m'ont surpris, car ce n'est pas dans nos habituelles puissances de 2. Mais en fait, ASCII fait partie d'une époque où la mémoire et les disques étaient vraiment très cher et il valait donc vraiment mieux ne pas être trop gourmand.

    As-tu lu le lien que tu pointes? Ça parle d'autres choses, comme le fait que 6 bits étaient un peu juste (pas de minuscules et beaucoup de code de contrôle) et 8 bits était pas vu comme utile pour les américains qui se foutaient du reste du monde, et surtout pas mal de monde trouvait plus utile d'utiliser le 8ème bit pour d'autres choses suivant le besoin (contrôle de parité par exemple) et du coup par exemple les serveurs mails le filtrait.
    Corrigez-moi si je me trompe mais il ne me semble pas avoir lu que les développeurs "optimisaient" à l'époque en stockant 8 caractères sur 7 octets mais stockaient sur 8 octets, en tous cas je ne connais aucun format de stockage faisant ça.

    Ils donnent beaucoup d'information à propos d'UTF-8 vs UTF-16 vs UTF-32 et pourquoi ils pensent que c'était inutile d'inventer wchar_t que finalement seul std::string était utile au C++3.

    C'est facile en regardant l'histoire avec sa vue de maintenant, mais à une époque on pariait sur :
    - qu'on resterait sur 16-bit max (d'où wchar_t sur 16-bit sous Windows)
    - qu'on aurait (hormis 0x00-0x20 et 0x7F "interdits" pour les humains) que des caractères
    - ça évitait une transformation (on avait en entrée du 8-bit et le charset, on transformait le tout en wchar_t pour traiter partout de la même manière)
    et du coup wchar_t permettait d'accéder facilement au x-ième caractère.
    Alors certes aujourd'hui il ne faut pas utiliser wchar_t mais garder en mémoire qu'avant on ne savait pas que ça irait dans une autre direction, avec UTF-8 partout pour le stockage (en acceptant une perte de stockage pour les chinois, l'histoire aurait pu dire que UTF-16 était mieux pour optimiser l'espace), plus de 16 bits et des caractères Unicode de transformation.
    Ce n'était pas inutile à l'époque de son invention et où on ne savait pas comment serait le futur, c'est devenu inutile à gérer de nos jours, comme plein d'autres formats.
    D'ailleurs, ton lien utf8everywhere parle surtout de promouvoir UTF-8 partout en connaissant le présent et c'est tout.

    • [^] # Re: Hum...

      Posté par  (site web personnel, Mastodon) . Évalué à 4.

      As-tu lu le lien que tu pointes?

      Oui, première réponse de mon lien (l'emphase est de moi)

      Before there was ASCII, there was BCDIC. BCDIC (not EBCDIC) was a 6 bit code with 64 combinations. This was enough to represent the upper case alphabet, numbers and common punctuation. When ASCII was adopted, another bit was added to include the lower case character set, but there was not seen to be a need for an eighth bit, since there were no teleprinters that could handle a larger character set. So the eighth bit became an optional parity bit.

      Readers of a certain age may remember that back when memory was expensive, video terminals and personal computers very often only supported the first 64 ASCII characters that fit into 6 bits, because that allowed you to use a 1k ROM for a character generator.


      C'est facile en regardant l'histoire avec sa vue de maintenant, mais à une époque on pariait sur :

      Oui, c'est facile maintenant… Et alors ? Mon point était que ce document m'a confirmé qu'aujourd'hui, il n'y a pas besoin de wchar pour moi.

    • [^] # Re: Hum...

      Posté par  (site web personnel, Mastodon) . Évalué à 10. Dernière modification le 05 septembre 2022 à 13:58.

      Cette réponse donne l'occasion de faire encore un peu d'archéologie (ou plutôt juste de l'histoire vu que c'est encore en usage) :)

      Les 7 bits m'ont surpris, car ce n'est pas dans nos habituelles puissances de 2. Mais en fait, ASCII fait partie d'une époque où la mémoire et les disques étaient vraiment très cher et il valait donc vraiment mieux ne pas être trop gourmand.

      As-tu lu le lien que tu pointes? Ça parle d'autres choses, comme le fait que 6 bits étaient un peu juste (pas de minuscules et beaucoup de code de contrôle) et 8 bits était pas vu comme utile pour les américains qui se foutaient du reste du monde, et surtout pas mal de monde trouvait plus utile d'utiliser le 8ème bit pour d'autres choses suivant le besoin (contrôle de parité par exemple) et du coup par exemple les serveurs mails le filtrait.

      En fait, on a utilisé 5 bits pendant longtemps, notamment dans les codes télégraphiques comme Beaudot (ITA1) dès les années 1870
      An early version from Baudot's 1888 US patent
      (Je remonte jusqu'au télégraphe parce-que les téléscripteurs ont été utilisé avec les premiers ordinateurs)
      On pourrait citer aussi le code Bacon de 1605 si on veut remonter plus loin.

      En 1901, une autre variante, Muray (ITA2) est actée et standardise certains caractères de contrôle (CR et LF mais aussi Del et Bell)
      British variant of ITA2
      (c'est elle qui sera utilisée avec les premières cartes perforées, sur les Linotype et une bonne partie de l'informatique à l'époque)

      On note que jusque là on ne distingue pas la casse, tradition qu'on retrouve dans certains systèmes (par exemple VMS dont descendent CPM et DOS) et langages (BASIC). Le passage à 6 bits va permettre d'avoir, dès fin 1950 avec FIELDATA et EBCDIC d'avoir les chiffres et divers symboles ou d'autres caractères de contrôle sur certains systèmes (UNIVAC, SDS, mainframes IBM)
      Military code and UNIVAC graphical code

      En arrivant entre 1961 et 1963, US-ASCII n'était que dans la même continuité et a eu besoin de 7 bits pour caser un peu plus de symboles et encore plus de caractères de contrôles qui vont être standardisées du coup. bien qu'ayant essayé de rester compatibles avec les existants (notamment EBCDIC pour les glyphes et les contrôles possibles), il y a eu une réorganisation interne pour faciliter l'utilisation…
      ASCII chart from a pre-1972 printer manual

      Il eut été possible d'utiliser 6 bits avec un caractère de contrôle pour dire qu'on bascule en minuscules (c'était une pratique courante), mais il a été jugé plus simple d'avoir un bit spécifique pour les minuscules (bascule shift) et un autre pour les trente-deux codes de contrôle, d'où le total de 7 (résumé de ma traduction du passage suivant)
      « The committee debated the possibility of a shift function (like in ITA2), which would allow more than 64 codes to be represented by a six-bit code. In a shifted code, some character codes determine choices between options for the following character codes. It allows compact encoding, but is less reliable for data transmission, as an error in transmitting the shift code typically makes a long part of the transmission unreadable. The standards committee decided against shifting, and so ASCII required at least a seven-bit code. »

      Maintenant, il ne faut pas oublier que les deux premières lettres de ASCII veulent dire American Standard : ils avaient besoin de standardiser les choses pour leur petit continent… Ce n'est pas qu'ils en avaient rien à foutre du reste du monde mais c'était vraiment anecdotique vu le vaste marché intérieur.
      Ainsi, Eric N. Fischer nous apprend que la firme de Baudot a prévu d'utiliser 6 bits (c'est juste non standardisés) pour les codes de contrôle ou d'autres alphabets. Le même principe est repris par EBCDIC avec les pages de code puis avec le 8ème bit de l'ASCII étendu

      Corrigez-moi si je me trompe mais il ne me semble pas avoir lu que les développeurs "optimisaient" à l'époque en stockant 8 caractères sur 7 octets mais stockaient sur 8 octets, en tous cas je ne connais aucun format de stockage faisant ça.

      Effectivement, ce n'était pas le cas pour le stockage sur disque… Peut-être qu'avec des cartes perforées, faut que je vérifie. Par contre, le standard est sur 7 bits car pensé pour faire des transmissions télégraphiques (donc pas que pour l'informatique) tout en restant compatible avec le stockage d'octets ; c'est ainsi que je comprends le passage suivant
      « The committee considered an eight-bit code, since eight bits (octets) would allow two four-bit patterns to efficiently encode two digits with binary-coded decimal. However, it would require all data transmission to send eight bits when seven could suffice. The committee voted to use a seven-bit code to minimize costs associated with data transmission. Since perforated tape at the time could record eight bits in one position, it also allowed for a parity bit for error checking if desired. Eight-bit machines (with octets as the native data type) that did not use parity checking typically set the eighth bit to 0. » associé à cet autre passage
      « ASCII was first used commercially during 1963 as a seven-bit teleprinter code for American Telephone & Telegraph's TWX (TeletypeWriter eXchange) network. TWX originally used the earlier five-bit ITA2, which was also used by the competing Telex teleprinter system. Bob Bemer introduced features such as the escape sequence. His British colleague Hugh McGregor Ross helped to popularize this work – according to Bemer, "so much so that the code that was to become ASCII was first called the Bemer–Ross Code in Europe". Because of his extensive work on ASCII, Bemer has been called "the father of ASCII". »

      Alors certes aujourd'hui il ne faut pas utiliser wchar_t mais garder en mémoire qu'avant on ne savait pas que ça irait dans une autre direction, avec UTF-8 partout pour le stockage (en acceptant une perte de stockage pour les chinois, l'histoire aurait pu dire que UTF-16 était mieux pour optimiser l'espace), plus de 16 bits et des caractères Unicode de transformation.

      Exactement. C'est aussi ce qui est dit de l'ASCII étendu

      « Eventually, as 8-, 16-, and 32-bit (and later 64-bit) computers began to replace 12-, 18-, and 36-bit computers as the norm, it became common to use an 8-bit byte to store each character in memory, providing an opportunity for extended, 8-bit relatives of ASCII. In most cases these developed as true extensions of ASCII, leaving the original character-mapping intact, but adding additional character definitions after the first 128 (i.e., 7-bit) characters. »
      (et du coup je n'ai pas compris pourquoi le moinssage)

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Hum...

        Posté par  (site web personnel, Mastodon) . Évalué à 3.

        Merci, je garde ça pour ma prochaine dépêche dans la série Transimpressux. C'est un chapitre que j'avais prévu.

        « Tak ne veut pas quʼon pense à lui, il veut quʼon pense », Terry Pratchett, Déraillé.

        • [^] # Re: Hum...

          Posté par  (site web personnel, Mastodon) . Évalué à 2.

          Merci W… qui m'a permis de ne pas dire de connerie en me basant juste sur ma mémoire et d'avoir pointé de belles références que je n'avais pas en favoris :-)
          Mais tu me rappelles que je dois finir une dépêche et un journal. Faut que je me trouve le temps…

          “It is seldom that liberty of any kind is lost all at once.” ― David Hume

          • [^] # Re: Hum...

            Posté par  (site web personnel, Mastodon) . Évalué à 3.

            En fait ça ne sera pas la prochaine, et en j'ai aussi une dépêche en train sur LibreOffice et elle ne se fait pas toute seule.

            J'y retourne (ou pas).

            « Tak ne veut pas quʼon pense à lui, il veut quʼon pense », Terry Pratchett, Déraillé.

            • [^] # Re: Hum...

              Posté par  (site web personnel) . Évalué à 3.

              Essaie les dépêches bateaux ou les dépêches en cycle peut-être ?

              • [^] # Re: Hum...

                Posté par  (site web personnel, Mastodon) . Évalué à 2.

                que sont-ce ?

                “It is seldom that liberty of any kind is lost all at once.” ― David Hume

              • [^] # Re: Hum...

                Posté par  (site web personnel, Mastodon) . Évalué à 4.

                La dépêche sur LibreOffice risque d'être bateau quand la prochaine du cycle Transimpressux parlera probablement d'encyclique.

                « Tak ne veut pas quʼon pense à lui, il veut quʼon pense », Terry Pratchett, Déraillé.

            • [^] # Re: Hum...

              Posté par  (site web personnel, Mastodon) . Évalué à 2.

              J'ai vu passer LibreOffice :) Pas encore vu la suite de la série Transimpressux ;p

              “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Hum...

        Posté par  (site web personnel) . Évalué à 4.

        les deux premières lettres de ASCII veulent dire American Standard : ils avaient besoin de standardiser les choses pour leur petit continent

        s/continent/pays/

        Il y a des pays en Amérique (même du Nord) qui ont besoin de plus qu'ASCII.

        pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

        • [^] # Re: Hum...

          Posté par  (site web personnel, Mastodon) . Évalué à 2.

          Oui, l'espagnol étant une langue majoritaire on ne s'étonne pas de la présence rapidement de certains caractères (et la disposition us-intl permet de saisir la plupart des voyelles accentuées que nous utilisons, en fait tous les caractères du premier extended-ASCII) Mais c'est vrai que ce n'y était pas de base et si on remet dans le contexte (i.e. l'année d'introduction et les usagers concernés —on parlait de télégraphie qui se voulait international mais le bouillon de cultures n'était qu'entre yankees) ce n'est pas surprenant (ou peut-être que c'est juste moi qui me suis laissé piéger par le double sens d'Amérique…)
          Les écriture aborigènes du Canada (reconnues sur la disposition ca-multilingue) n'étaient pas bien connues à l'époque il me semble, et leur entrée dans l'informatique est arrivée avec l'Unicode il me semble.

          “It is seldom that liberty of any kind is lost all at once.” ― David Hume

    • [^] # Re: Hum...

      Posté par  . Évalué à 2.

      La ou ça me gène en pratique, c'est qu'il y a très peu (aucune ?) de fonctions de conversion.

      Autant je peux comprendre qu'historiquement on savait pas forcément si UTF-16 ou 8 ou 32 (ou 7… si si ça existe même s'il était sans doute pas candidat à la victoire :p) allait gagner et donc comment représenter une chaine (bon, en pratique, on représente qu'un tableau, osef d'itérer sur des charactères, savoir la taille de la chaine etc biensur), autant je trouve completement ridicule d'avoir aussi peu d'outils en C de base pour convertir ce qui fait que toute l'api te pousse à faire des erreurs.

      Rappel, ce qui est considéré comme une bonne pratique est d'avoir une représentation interne connue et de convertir à la lecture/écriture depuis/dans le charset qu'on veut, et en C c'est bien galère.

      De même, travailler sur des chaines en C, faut vraiment aimer les bugs, difficile de passer en lower case, de savoir le nombre de charactères, de normaliser…

  • # Ça me rappelle un moment d'extrême souffrance !

    Posté par  . Évalué à 3.

    Un jour, j'ai eu une vision d'horreur :

    printf("sizeof(uint8_t):%d\n",sizeof(uint8_t));
    

    m'a renvoyé

    2
    

    Un peu de contexte : tentative de codage d'appli sur puce Bluetooth CSR 8760.
    Sur le papier : super, cette puce dispose d'un MCU pour l'appli, d'un DSP pour les traitements audio, oui monsieur, vous pouvez porter votre appli MCU et DSP AD AU1701 sans problème.

    En pratique … TLD;DR : abandon, trop peu de ressources MCU et DSP calculant sur des entiers, et non pas sur des flottants comme le 1701, donc portage non garanti. Après une tentative en délaissant le DSP intégré et en remettant le 1701.

    Pour revenir sur la taille de 2 pour un uint8_t, le support ne m'a pas aidé. C'est moi qui ait trouvé la réponse dans le support CSR pour une autre puce. Et c'est comme pointé dans l'article : c'est autorisé, la norme C permet de faire de genre de cascade.

    Donc, OK, si c'est dans la norme, je veux bien.

    Mais, si on passe sur le fait que l'unique fonction permettant de transférer des données par le bus I2C sur CSR 8760 est totalement pourrave, quand je lui donne un pointeur sur le début des data, qu'est-ce qui va être transféré précisément ? Un octet sur deux de mon buffer ? Comment je prépare mon buffer ? Et si mes données proviennent d'un fichier du pseudo filesystem en flash, est-ce que je doit doubler sa taille pour insérer 8 bits tous les 8 bits ?

    Ah, non, j'oubliais que la doc de cette fonction I2C était aussi naze : elle dit que des problème audio peuvent survenir si on transfère plus de 64 octets (m'en fout, c'est pour programmer le DSP, donc pas d'audio à ce moment-là). En fait, il faut comprendre : cette fonction est incapable de transférer plus de 64. C'est ballot, le code du 1701 est découpé en 5 buffers, dont un de 4 et l'autre de 5 ko …

    Deux ou trois mois de perdus à tenter de comprendre comment cette puce avait été survendue. Pendant que Qualcom était en train de racheter cette boite …

    Moi ? Plein de rancœur à l'encontre des perfides "Cambridgiens" ? Si peu …

    • [^] # Re: Ça me rappelle un moment d'extrême souffrance !

      Posté par  (site web personnel) . Évalué à 3.

      uint8_t […] Et c'est comme pointé dans l'article : c'est autorisé, la norme C permet de faire de genre de cascade.

      Euh… non.
      l'article parle de de char, pas de de uint8_t.
      Je ne vois pas où la norme permet ce que tu dis.

      "uint8_t: unsigned integer type with width of exactly 8".

      Le mot important est "exactly" dans le cas des entier avec taille précise (ce qui n'est pas le cas de short ou int par exemple, "at least").

      De plus "1 == sizeof(char)" est garanti et certes ça ne dit pas exactement pour uint8_t mais comme celui-la est fortement lié à char et que char ne fait pas moins de 8 bits… je comprend que pour que tu ais 2 dans ton exemple il faudrait un CPU qui travaille sur des multiples de 7 bits, je doute…

  • # Le saviez-vous ?

    Posté par  (Mastodon) . Évalué à 5. Dernière modification le 05 septembre 2022 à 14:29.

    Il faut faire attention avec char, parce qu'il peut être signé ou non-signé. Et que, c'est là que c'est drôle, il n'est équivalent ni à signed char ni à unsigned char (dans le sens où ce sont trois types différents). Et enfin, ça fout toujours le bazar d'avoir des pointeurs sur char parce que les règles d'aliasing disent que char * peut représenter n'importe quoi, un peu comme void *, et donc ça empêche plein d'optimisations.

    À noter qu'il existe depuis C++20 le type char8_t (ainsi que char16_t et char32_t depuis C++11) pour représenter un code unit UTF8 (respectivement UTF16 et UTF32). Je crois que ça va arriver bientôt en C (les deux autres y sont déjà depuis C11).

  • # Ebook Programming with Unicode

    Posté par  (site web personnel) . Évalué à 8.

    Bonjour, j'ai écrit l'ebook gratuit https://unicodebook.readthedocs.io/ qui parle des charsets, encodings, Unicode, etc.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.