C-Programmierung: Wie programmiere ich für Unicode?

82

Welche Voraussetzungen sind für eine strikte Unicode-Programmierung erforderlich?

Bedeutet dies, dass mein Code charnirgendwo Typen verwenden sollte und dass Funktionen verwendet werden müssen, die mit wint_tund umgehen können wchar_t?

Und welche Rolle spielen Multibyte-Zeichenfolgen in diesem Szenario?

prinzdezibel
quelle

Antworten:

21

Beachten Sie, dass es nicht um "strikte Unicode-Programmierung" an sich geht, sondern um praktische Erfahrung.

In meinem Unternehmen haben wir eine Wrapper-Bibliothek um die IBM ICU-Bibliothek erstellt. Die Wrapper-Bibliothek verfügt über eine UTF-8-Schnittstelle und wird in UTF-16 konvertiert, wenn die Intensivstation aufgerufen werden muss. In unserem Fall haben wir uns nicht allzu viele Sorgen um Performance-Hits gemacht. Wenn die Leistung ein Problem war, haben wir auch UTF-16-Schnittstellen bereitgestellt (unter Verwendung unseres eigenen Datentyps).

Anwendungen können weitgehend unverändert bleiben (mit char), obwohl sie in einigen Fällen bestimmte Probleme berücksichtigen müssen. Beispielsweise verwenden wir anstelle von strncpy () einen Wrapper, der das Abschneiden von UTF-8-Sequenzen vermeidet. In unserem Fall ist dies ausreichend, aber man könnte auch Prüfungen zum Kombinieren von Zeichen in Betracht ziehen. Wir haben auch Wrapper zum Zählen der Anzahl von Codepunkten, der Anzahl von Graphemen usw.

Bei der Schnittstelle mit anderen Systemen müssen wir manchmal eine benutzerdefinierte Zeichenkomposition durchführen, sodass Sie dort möglicherweise etwas Flexibilität benötigen (abhängig von Ihrer Anwendung).

Wir verwenden wchar_t nicht. Durch die Verwendung der Intensivstation werden unerwartete Probleme bei der Portabilität vermieden (aber natürlich keine anderen unerwarteten Probleme :-).

Hans van Eck
quelle
2
Eine gültige UTF-8-Byte-Sequenz würde niemals von strncpy abgeschnitten (abgeschnitten). Gültige UTF-8-Sequenzen dürfen keine 0x00 Bytes enthalten (außer natürlich das terminierende Null-Byte).
Dan Moulding
8
@ Dan Moulding: Wenn Sie beispielsweise strncpy (), eine Zeichenfolge mit einem einzelnen chinesischen Zeichen (das 3 Byte umfassen kann) in ein 2-Byte-Zeichenarray einfügen, erstellen Sie eine ungültige UTF-8-Sequenz.
Hans van Eck
@ Hans van Eck: Wenn Ihr Wrapper dieses einzelne chinesische 3-Byte-Zeichen in ein 2-Byte-Array kopiert, werden Sie es entweder abschneiden und eine ungültige Sequenz erstellen, oder Sie werden ein undefiniertes Verhalten haben. Wenn Sie Daten kopieren, muss das Ziel natürlich groß genug sein. das ist selbstverständlich. Mein Punkt war, dass strncpydie richtige Verwendung mit UTF-8 absolut sicher ist.
Dan Moulding
5
@DanMoulding: Wenn Sie wissen, dass Ihr Zielpuffer groß genug ist, können Sie ihn einfach verwenden strcpy(was in der Tat sicher mit UTF-8 zu verwenden ist). Benutzer verwenden dies strncpywahrscheinlich, weil sie nicht wissen, ob der Zielpuffer groß genug ist. Daher möchten sie eine maximale Anzahl von zu kopierenden Bytes übergeben - was tatsächlich zu ungültigen UTF-8-Sequenzen führen kann.
Frerich Raabe
41

C99 oder früher

Der C-Standard (C99) sieht breite Zeichen und Mehrbyte-Zeichen vor. Da jedoch keine Garantie dafür besteht, was diese breiten Zeichen enthalten können, ist ihr Wert etwas begrenzt. Für eine bestimmte Implementierung bieten sie nützliche Unterstützung. Wenn Ihr Code jedoch zwischen Implementierungen wechseln kann, gibt es keine ausreichende Garantie dafür, dass sie nützlich sind.

Folglich ist der von Hans van Eck vorgeschlagene Ansatz (der darin besteht, einen Wrapper um die ICU - International Components for Unicode - Bibliothek zu schreiben) solide, IMO.

Die UTF-8-Codierung hat viele Vorteile. Wenn Sie nicht mit den Daten herumspielen (z. B. durch Abschneiden), kann sie von Funktionen kopiert werden, die die Feinheiten von UTF-8 nicht vollständig kennen Codierung. Dies ist bei kategorisch nicht der Fall wchar_t.

Unicode in Full ist ein 21-Bit-Format. Das heißt, Unicode reserviert Codepunkte von U + 0000 bis U + 10FFFF.

Eines der nützlichen Dinge bei den Formaten UTF-8, UTF-16 und UTF-32 (wobei UTF für Unicode-Transformationsformat steht - siehe Unicode ) ist, dass Sie ohne Informationsverlust zwischen den drei Darstellungen konvertieren können. Jeder kann alles darstellen, was der andere darstellen kann. Sowohl UTF-8 als auch UTF-16 sind Multi-Byte-Formate.

UTF-8 ist bekanntermaßen ein Multi-Byte-Format mit einer sorgfältigen Struktur, die es ermöglicht, den Anfang von Zeichen in einer Zeichenfolge zuverlässig zu finden, beginnend an jedem Punkt in der Zeichenfolge. Bei Einzelbyte-Zeichen ist das High-Bit auf Null gesetzt. Bei Mehrbytezeichen beginnt das erste Zeichen mit einem der Bitmuster 110, 1110 oder 11110 (für 2-Byte-, 3-Byte- oder 4-Byte-Zeichen), wobei nachfolgende Bytes immer mit 10 beginnen. Die Fortsetzungszeichen befinden sich immer in der Bereich 0x80 .. 0xBF. Es gibt Regeln, nach denen UTF-8-Zeichen im minimal möglichen Format dargestellt werden müssen. Eine Konsequenz dieser Regeln ist, dass die Bytes 0xC0 und 0xC1 (auch 0xF5..0xFF) nicht in gültigen UTF-8-Daten erscheinen können.

 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

Ursprünglich hoffte man, dass Unicode ein 16-Bit-Code-Set sein würde und alles in einen 16-Bit-Code-Raum passen würde. Leider ist die reale Welt komplexer und musste auf die aktuelle 21-Bit-Codierung erweitert werden.

UTF-16 ist somit ein Code mit einer Einheit (16-Bit-Wort), der für die 'Basic Multilingual Plane' festgelegt wurde, dh die Zeichen mit den Unicode-Codepunkten U + 0000 .. U + FFFF, verwendet jedoch zwei Einheiten (32 Bit) für Zeichen außerhalb dieses Bereichs. Daher muss Code, der mit der UTF-16-Codierung funktioniert, Codierungen mit variabler Breite verarbeiten können, genau wie UTF-8. Die Codes für die Zeichen mit zwei Einheiten werden als Ersatzzeichen bezeichnet.

Surrogate sind Codepunkte aus zwei speziellen Bereichen von Unicode-Werten, die für die Verwendung als führende und nachfolgende Werte von gepaarten Codeeinheiten in UTF-16 reserviert sind. Führende, auch hoch bezeichnete Surrogate reichen von U + D800 bis U + DBFF, und nachfolgende oder niedrige Surrogate reichen von U + DC00 bis U + DFFF. Sie werden als Ersatzzeichen bezeichnet, da sie keine Zeichen direkt darstellen, sondern nur als Paar.

UTF-32 kann natürlich jeden Unicode-Codepunkt in einer einzelnen Speichereinheit codieren. Es ist effizient für die Berechnung, aber nicht für die Speicherung.

Weitere Informationen finden Sie auf den Websites ICU und Unicode.

C11 und <uchar.h>

Der C11-Standard hat die Regeln geändert, aber nicht alle Implementierungen haben die Änderungen bereits jetzt (Mitte 2017) eingeholt. Der C11-Standard fasst die Änderungen für die Unicode-Unterstützung wie folgt zusammen:

  • Unicode-Zeichen und Zeichenfolgen ( <uchar.h>) (ursprünglich in ISO / IEC TR 19769: 2004 angegeben)

Was folgt, ist ein minimaler Überblick über die Funktionalität. Die Spezifikation enthält:

6.4.3 Universelle Charakternamen

Syntax
Universalzeichenname:
    \u Hex-Quad
    \U Hex-Quad Hex-Quad
Hex-Quad:
    Hexadezimal-Ziffer Hexadezimal-Ziffer Hexadezimal-Ziffer Hexadezimal-Ziffer

7.28 Unicode-Dienstprogramme <uchar.h>

Der Header <uchar.h>deklariert Typen und Funktionen zum Bearbeiten von Unicode-Zeichen.

Die deklarierten Typen sind mbstate_t(beschrieben in 7.29.1) und size_t(beschrieben in 7.19);

char16_t

uint_least16_tDies ist ein vorzeichenloser Integer-Typ, der für 16-Bit-Zeichen verwendet wird und der gleiche Typ ist wie (beschrieben in 7.20.1.2). und

char32_t

uint_least32_tDies ist ein vorzeichenloser Integer-Typ, der für 32-Bit-Zeichen verwendet wird und der gleiche Typ ist wie (auch in 7.20.1.2 beschrieben).

(Übersetzen der Querverweise: <stddef.h>definiert size_t, <wchar.h>definiert mbstate_tund <stdint.h>definiert uint_least16_tund uint_least32_t.) Der <uchar.h>Header definiert auch einen minimalen Satz von (neu startbaren) Konvertierungsfunktionen:

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

Es gibt Regeln, nach denen Unicode-Zeichen in Bezeichnern verwendet werden können, die die Notationen \unnnnoder \U00nnnnnnverwenden. Möglicherweise müssen Sie die Unterstützung für solche Zeichen in Bezeichnern aktiv aktivieren. Zum Beispiel muss GCC -fextended-identifiersdiese in Bezeichnern zulassen.

Beachten Sie, dass macOS Sierra (10.12.5), um nur eine Plattform zu nennen, keine Unterstützung bietet <uchar.h>.

Jonathan Leffler
quelle
3
Ich denke, Sie verkaufen wchar_tund Freunde hier ein bisschen kurz. Diese Typen sind wichtig, damit die C-Bibliothek Text in jeder Codierung (einschließlich Nicht-Unicode-Codierungen) verarbeiten kann. Ohne die umfangreichen Zeichentypen und -funktionen würde die C-Bibliothek für jede unterstützte Codierung eine Reihe von Textverarbeitungsfunktionen erfordern : Stellen Sie sich vor, Sie hätten koi8len, koi8tok, koi8printf nur für KOI-8-codierten Text und utf8len, utf8tok, utf8printf für UTF-8 Text. Stattdessen haben wir das Glück nur zu haben , eine Reihe dieser Funktionen (nicht die Original - ASCII diejenigen mitgezählt): wcslen, wcstok, und wprintf.
Dan Moulding
1
Ein Programmierer muss mbstowcslediglich die Zeichenkonvertierungsfunktionen der C-Bibliothek ( und Freunde) verwenden, um die unterstützte Codierung in zu konvertieren wchar_t. Nach dem wchar_tFormatieren kann der Programmierer die einzelnen Funktionen zur Verarbeitung von Breittexten verwenden, die die C-Bibliothek bietet. Eine gute Implementierung der C-Bibliothek unterstützt praktisch jede Codierung, die die meisten Programmierer jemals benötigen werden (auf einem meiner Systeme habe ich Zugriff auf 221 eindeutige Codierungen).
Dan Moulding
Soweit sie breit genug sind, um nützlich zu sein: Der Standard erfordert eine Implementierung, die sicherstellen muss, dass sie wchar_tbreit genug ist, um alle von der Implementierung unterstützten Zeichen aufzunehmen. Dies bedeutet (mit möglicherweise einer bemerkenswerten Ausnahme), dass die meisten Implementierungen sicherstellen, dass sie breit genug sind, dass ein Programm, das verwendet wchar_t, alle vom System unterstützten Codierungen verarbeitet (Microsoft wchar_tist nur 16 Bit breit, was bedeutet, dass ihre Implementierung nicht alle Codierungen vollständig unterstützt). vor allem die verschiedenen UTF-Codierungen, aber ihre ist die Ausnahme, nicht die Regel).
Dan Moulding
11

Diese FAQ ist eine Fülle von Informationen. Zwischen dieser Seite und diesem Artikel von Joel Spolsky haben Sie einen guten Anfang.

Eine Schlussfolgerung, zu der ich unterwegs gekommen bin:

  • wchar_tist 16 Bit unter Windows, aber nicht unbedingt 16 Bit auf anderen Plattformen. Ich denke, es ist ein notwendiges Übel unter Windows, kann aber wahrscheinlich woanders vermieden werden. Unter Windows ist es wichtig, dass Sie Dateien verwenden müssen, deren Name Nicht-ASCII-Zeichen enthält (zusammen mit der W-Version der Funktionen).

  • Beachten Sie, dass Windows-APIs, die wchar_tZeichenfolgen verwenden, eine UTF-16-Codierung erwarten. Beachten Sie auch, dass dies anders ist als UCS-2. Beachten Sie die Ersatzpaare. Diese Testseite enthält aufschlussreiche Tests.

  • Wenn Sie sich die Programmierung unter Windows können Sie nicht verwenden fopen(), fread(), fwrite()usw. , da sie nur nehmen char *und nicht UTF-8 - Codierung verstehen. Macht die Portabilität schmerzhaft.

dbyron
quelle
Beachten Sie, dass stdio f*und Freunde char *auf jeder Plattform arbeiten, weil der Standard dies wcs*vorschreibt - verwenden Sie stattdessen wchar_t.
Katze
7

So führen Sie eine strikte Unicode-Programmierung durch:

  • Nur String - APIs verwenden , die Unicode bewusst sind ( NICHT strlen , strcpy... aber ihre Pendants Wide wstrlen, wsstrcpy...)
  • Verwenden Sie beim Umgang mit einem Textblock eine Codierung, mit der Unicode-Zeichen (utf-7, utf-8, utf-16, ucs-2, ...) ohne Verlust gespeichert werden können.
  • Überprüfen Sie, ob der Standardzeichensatz Ihres Betriebssystems Unicode-kompatibel ist (Beispiel: utf-8).
  • Verwenden Sie Unicode-kompatible Schriftarten (z. B. arial_unicode).

Multi-Byte-Zeichenfolgen sind eine Codierung, die vor der UTF-16-Codierung (die normalerweise verwendet wird wchar_t) datiert, und es scheint mir, dass sie eher nur Windows ist.

Ich habe noch nie davon gehört wint_t.

sebastien
quelle
wint_t ist ein in <wchar.h> definierter Typ, genau wie wchar_t. Es hat die gleiche Rolle in Bezug auf breite Zeichen, die int in Bezug auf 'char' hat; Es kann einen beliebigen breiten Zeichenwert oder WEOF enthalten.
Jonathan Leffler
3

Das Wichtigste ist, immer klar zwischen Text und Binärdaten zu unterscheiden . Versuchen Sie, dem Modell von Python 3.x strvs.bytes oder SQL TEXTvs. zu folgen BLOB.

Leider verwirrt C das Problem, indem es charsowohl für "ASCII-Zeichen" als auch für "ASCII-Zeichen" verwendet int_least8_t. Sie möchten etwas tun wie:

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

Möglicherweise möchten Sie auch Typedefs für UTF-16- und UTF-32-Codeeinheiten, dies ist jedoch komplizierter, da die Codierung von wchar_tnicht definiert ist. Sie benötigen nur einen Präprozessor #if. Einige nützliche Makros in C und C ++ 0x sind:

  • __STDC_UTF_16__- Wenn definiert, _Char16_texistiert der Typ und ist UTF-16.
  • __STDC_UTF_32__- Wenn definiert, _Char32_texistiert der Typ und ist UTF-32.
  • __STDC_ISO_10646__- Wenn definiert, wchar_tist UTF-32.
  • _WIN32- Unter Windows wchar_tist UTF-16, obwohl dies den Standard bricht.
  • WCHAR_MAX- Kann verwendet werden, um die Größe von zu bestimmen wchar_t, aber nicht, ob das Betriebssystem es zur Darstellung von Unicode verwendet.

Bedeutet dies, dass mein Code nirgendwo Zeichentypen verwenden sollte und dass Funktionen verwendet werden müssen, die mit wint_t und wchar_t umgehen können?

Siehe auch:

Nein. UTF-8 ist eine perfekt gültige Unicode-Codierung, die char*Zeichenfolgen verwendet. Dies hat den Vorteil, dass Sie überhaupt keine Änderungen vornehmen müssen , wenn Ihr Programm für Nicht-ASCII-Bytes transparent ist (z. B. ein Konverter mit Zeilenende, der auf andere Zeichen einwirkt \rund \ndiese unverändert durchläuft).

Wenn Sie sich für UTF-8 entscheiden, müssen Sie alle Annahmen ändern, die char= Zeichen (z. B. keine toupperSchleife aufrufen ) oder char= Bildschirmspalte (z. B. zum Umbrechen von Text).

Wenn Sie sich für UTF-32 entscheiden, haben Sie die Einfachheit von Zeichen mit fester Breite (aber keine Grapheme mit fester Breite , sondern müssen den Typ aller Ihrer Zeichenfolgen ändern).

Wenn Sie sich für UTF-16 entscheiden, müssen Sie sowohl die Annahme von Zeichen mit fester Breite als auch die Annahme von 8-Bit- Codeeinheiten verwerfen , was dies zum schwierigsten Upgrade-Pfad für Einzelbyte-Codierungen macht.

Ich würde empfehlen, aktiv zu vermeiden, wchar_t da es nicht plattformübergreifend ist: Manchmal ist es UTF-32, manchmal ist es UTF-16 und manchmal ist es eine ostasiatische Codierung vor Unicode. Ich würde empfehlen, zu verwendentypedefs

Noch wichtiger ist, vermeidenTCHAR .

dan04
quelle
Ich denke nicht, dass das überhaupt unglücklich ist - der Char ist ein Int. Das ist ein Vorteil. Die Verwendung von wörtlichen Zeichenkonstanten fällt mir als eine Verwendung ein. Und Funktionen, die ein nehmen, char *können Probleme haben, wenn sie ein const char *letztes bestanden haben, an das ich mich erinnere (aber ich bin vage darüber und welche Funktionen nehmen Sie es mit einer Prise Salz). Nur weil es mit anderen Sprachen komplizierter ist, heißt das nicht, dass es ein schlechtes Design ist.
Pryftan
2

Ich würde keiner Standardbibliotheksimplementierung vertrauen. Rollen Sie einfach Ihre eigenen Unicode-Typen.

#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;
}

quelle
2

Grundsätzlich möchten Sie Zeichenfolgen im Speicher als wchar_tArrays anstelle von char behandeln. Wenn Sie irgendeine Art von E / A ausführen (wie das Lesen / Schreiben von Dateien), können Sie mit UTF-8 (dies ist wahrscheinlich die häufigste Codierung) codieren / decodieren, was einfach zu implementieren ist. Google einfach die RFCs. Im Speicher sollte also nichts mehr als Byte sein. Einer wchar_trepräsentiert ein Zeichen. Wenn Sie jedoch zur Serialisierung kommen, müssen Sie in etwas wie UTF-8 codieren, in dem einige Zeichen durch mehrere Bytes dargestellt werden.

Sie müssen auch neue Versionen von strcmpusw. für die breiten Zeichenketten schreiben , aber dies ist kein großes Problem. Das größte Problem wird die Interaktion mit Bibliotheken / vorhandenem Code sein, die nur Char-Arrays akzeptieren.

Und wenn es darum geht sizeof(wchar_t)(Sie benötigen 4 Bytes, wenn Sie es richtig machen wollen), können Sie es jederzeit mit typedef/ macrohacks auf eine größere Größe neu definieren, wenn Sie müssen.

Mike Weller
quelle
1

Soweit ich weiß, ist wchar_t implementierungsabhängig (wie aus diesem Wiki-Artikel hervorgeht ). Und es ist kein Unicode.

PolyThinker
quelle