Programowanie w C: jak programować dla Unicode?

Jakie warunki wstępne są potrzebne do ścisłego programowania Unicode?

Czy to oznacza, że mój kod nie powinien używać typów char nigdzie i że należy używać funkcji, które mogą radzić sobie z wint_t i wchar_t?

A jaką rolę odgrywają wielobajtowe sekwencje znaków w tym scenariuszu?

Author: Jonathan Leffler, 2009-02-09

8 answers

Zauważ, że nie chodzi tu o" ścisłe programowanie unicode " jako takie, ale o pewne praktyczne doświadczenie.

W mojej firmie stworzyliśmy bibliotekę wrapperów wokół biblioteki IBM ICU. Biblioteka opakowująca posiada interfejs UTF-8 i konwertuje do UTF-16, gdy jest to konieczne do wywołania ICU. W naszym przypadku nie martwiliśmy się zbytnio o hity performatywne. Gdy wydajność była problemem, dostarczaliśmy również interfejsy UTF-16 (używając własnego typu danych).

Aplikacje mogą pozostać w dużej mierze as-is (używając char), chociaż w niektórych przypadkach muszą być świadomi pewnych problemów. Na przykład, zamiast strncpy() używamy wrappera, który unika odcinania sekwencji UTF-8. W naszym przypadku jest to wystarczające, ale można również rozważyć sprawdzenie łączenia znaków. Posiadamy również wrappery do zliczania liczby punktów kodowych, liczby grafemów itp.

Podczas łączenia się z innymi systemami, czasami musimy wykonać niestandardową kompozycję znaków, więc możesz potrzebować pewnej elastyczności tam (w zależności od zastosowania).

Nie używamy wsar_t. używanie ICU pozwala uniknąć nieoczekiwanych problemów w przenośności (ale nie innych nieoczekiwanych problemów, oczywiście :-).

 20
Author: Hans van Eck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-08 22:44:06

C99 lub wcześniej

Standard C (C99) przewiduje szerokie znaki i znaki wielobajtowe, ale ponieważ nie ma gwarancji co do tego, co te szerokie znaki mogą pomieścić, ich wartość jest nieco ograniczona. Dla danej implementacji zapewniają one użyteczne wsparcie, ale jeśli twój kod musi być w stanie poruszać się między implementacjami, nie ma wystarczającej gwarancji, że będą przydatne.

W konsekwencji podejście zaproponowane przez Hansa van Ecka (czyli napisanie wrappera wokół ICU-International Components for Unicode-library) jest dźwięk, IMO.

Kodowanie UTF-8 ma wiele zalet, jedną z nich jest to, że jeśli nie zadzierasz z danymi (na przykład przez ich obcinanie), to mogą być one kopiowane przez funkcje, które nie są w pełni świadome zawiłości kodowania UTF-8. Tak kategorycznie nie jest w przypadku wchar_t.

Unicode w całości jest formatem 21-bitowym. Oznacza to, że Unicode rezerwuje Punkty kodowe od U + 0000 do U + 10FFFF.

Jeden z przydatne rzeczy na temat formatów UTF-8, UTF-16 i UTF-32 (gdzie UTF oznacza Format transformacji Unicode - zobacz Unicode) jest to, że można konwertować między trzema reprezentacjami bez utraty informacji. Każdy może reprezentować wszystko, co inni mogą reprezentować. Zarówno UTF - 8, jak i UTF-16 są formatami wielobajtowymi.

UTF-8 jest dobrze znany jako format wielobajtowy, ze staranną strukturą, która umożliwia niezawodne znalezienie początku znaków w ciągu znaków, począwszy od dowolny punkt w łańcuchu. Znaki jednobajtowe mają wysoki bit ustawiony na zero. Znaki wielbajtowe mają pierwszy znak zaczynający się od jednego z wzorców bitowych 110, 1110 lub 11110( dla znaków 2-bajtowych, 3-bajtowych lub 4-bajtowych), z kolejnymi bajtami zawsze zaczynającymi się od 10. Znaki kontynuacji są zawsze w zakresie 0x80 .. 0xBF. Istnieją reguły, że znaki UTF-8 muszą być reprezentowane w minimalnym możliwym formacie. Jedną z konsekwencji tych reguł jest to, że bajty 0xC0 i 0xc1 (także 0xF5..0xFF) nie może pojawić się w poprawnych danych UTF-8.

 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

Pierwotnie, miał nadzieję, że Unicode będzie 16-bitowym zestawem kodu i wszystko będzie pasować do 16-bitowej przestrzeni kodu. Niestety, świat rzeczywisty jest bardziej złożony i musiał zostać rozszerzony do obecnego 21-bitowego kodowania.

UTF-16 jest więc jednostkowym (16-bitowym) kodem ustawionym dla "podstawowej płaszczyzny wielojęzycznej", oznaczającym znaki z kodem Unicode U + 0000 .. U + FFFF, ale używa dwóch jednostek (32-bitowych) dla postaci spoza tego zakresu. Tak więc kod pracujący z kodowaniem UTF-16 musi być w stanie obsłużyć kodowanie o zmiennej szerokości, tak jak UTF-8 musi. Kody znaków dwudzielnych nazywane są zastępczymi.

Zastępcze są punktami kodu z dwóch specjalnych zakresów wartości Unicode, zarezerwowanych do użycia jako wartości początkowe i końcowe sparowanych jednostek kodu w UTF-16. Wiodące, zwane również wysokimi, surogatki są od U + D800 do U + DBFF, a końcowe, lub niskie, surogatki są od U + DC00 do U + DFFF. Nazywane są surogatami, ponieważ nie reprezentują znaków bezpośrednio, ale tylko jako para.

UTF-32 może oczywiście zakodować dowolny punkt kodu Unicode w jednej jednostce pamięci. Jest wydajny do obliczeń, ale nie do przechowywania.

Więcej informacji można znaleźć na stronach OIOM i Unicode.

C11 i <uchar.h>

Standard C11 zmienił reguły, ale nie wszystkie implementacje dogoniły zmiany już teraz (połowa 2017). Standard C11 podsumowuje zmiany w obsłudze Unicode jako:

  • znaki i ciągi znaków Unicode (<uchar.h>) (pierwotnie określone w ISO / IEC TR 19769: 2004)

Poniżej przedstawiamy minimalny zarys funkcjonalności. Specyfikacja zawiera:

6.4.3 uniwersalne nazwy znaków

Składnia
uniwersalny-znak-nazwa:
    \u hex-quad
    \U hex-quad hex-quad
hex-quad:
    hexadecimal-digit hexadecimal-digit hexadecimal-digit hexadecimal-digit

7.28 Unicode utilities <uchar.h>

Nagłówek <uchar.h> deklaruje typy i funkcje do manipulowania znakami Unicode.

Deklarowane typy to mbstate_t (opisane w 7.29.1) i size_t (opisane w 7.19);

char16_t

Który jest liczbą całkowitą niepodpisaną typ używany dla znaków 16-bitowych i jest tym samym typem co uint_least16_t (opisany w 7.20.1.2); oraz

char32_t

, który jest typem unsigned integer używanym dla 32-bitowych znaków i jest tym samym typem co uint_least32_t (opisanym również w 7.20.1.2).

(tłumaczenie odsyłaczy: <stddef.h> definiuje size_t, <wchar.h> definiuje mbstate_t, i <stdint.h> definiuje uint_least16_t i uint_least32_t.) Nagłówek <uchar.h> definiuje również minimalny zestaw funkcji konwersji (restartowalnych):

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

Istnieją reguły, które znaki Unicode mogą być używane w identyfikatorach przy użyciu notacji \unnnn lub \U00nnnnnn. Być może będziesz musiał aktywnie aktywować obsługę takich znaków w identyfikatorach. Na przykład, GCC wymaga -fextended-identifiers, aby zezwolić na te identyfikatory w identyfikatorach.

Zauważ, że macOS Sierra (10.12.5), aby wymienić tylko jedną platformę, nie obsługuje <uchar.h>.

 36
Author: Jonathan Leffler,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-11 02:26:22

Ten FAQ jest bogactwem informacji. Między tą stroną a tym artykułem Joela Spolsky ' ego, będziesz miał dobry początek.

Jeden wniosek, do którego doszedłem po drodze:

  • wchar_t jest 16 bitów w systemie Windows, ale niekoniecznie 16 bitów na innych platformach. Myślę, że to zło konieczne w Windows, ale prawdopodobnie można uniknąć gdzie indziej. Powodem, dla którego jest to ważne w systemie Windows, jest to, że potrzebujesz go, aby używać plików, które mają znaki inne niż ASCII w nazwie (wraz z w wersji funkcji).

  • Zauważ, że interfejsy API systemu Windows, które przyjmują ciągi wchar_t, oczekują kodowania UTF-16. Zauważ również, że jest to inne niż UCS-2. Zwróć uwagę na pary zastępcze. Ta strona testowa ma pouczające testy.

  • Jeśli programujesz w systemie Windows, nie możesz użyć fopen(), fread(), fwrite(), itd. ponieważ biorą tylko char * i nie rozumieją kodowania UTF-8. Sprawia, że przenośność jest bolesna.

 10
Author: dbyron,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-22 13:34:39

Aby wykonać ścisłe programowanie Unicode:

  • używaj tylko znaków API, które są świadome Unicode (Nie strlen, strcpy, ... ale ich szerokie odpowiedniki wstrlen, wsstrcpy, ...)
  • gdy mamy do czynienia z blokiem tekstu, użyj kodowania, które pozwala na przechowywanie znaków Unicode (utf-7, utf-8, utf-16, UCS-2,...) bez strat.
  • sprawdź, czy domyślny zestaw znaków systemu operacyjnego jest zgodny z Unicode (np: utf-8)
  • używać czcionek zgodnych z Unicode (np. arial_unicode)

Wielobajtowe sekwencje znaków to kodowanie, które wcześniej datuje kodowanie UTF-16 (to używane normalnie z wchar_t) i wydaje mi się, że jest to raczej tylko Windows.

Nigdy o tym nie słyszałem.
 7
Author: sebastien,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-27 01:07:50

Najważniejsze jest, aby zawsze wyraźnie rozróżniać dane tekstowe i binarne . Spróbuj podążać za modelem Pythona 3.x str vs.bytes lub SQL TEXT vs. BLOB.

Niestety, C myli problem, używając char zarówno dla "znaku ASCII", jak i int_least8_t. Będziesz chciał zrobić coś w stylu:

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

Możesz też chcieć typedefów dla jednostek kodu UTF-16 i UTF-32, ale jest to bardziej skomplikowane, ponieważ kodowanie wchar_t nie jest zdefiniowane. Niektóre przydatne makra w C i C++0x to:

  • {[9] } - jeśli jest zdefiniowany, typ _Char16_t istnieje i jest UTF-16.
  • __STDC_UTF_32__ - jeśli jest zdefiniowany, typ _Char32_t istnieje i jest UTF-32.
  • __STDC_ISO_10646__ - jeśli zdefiniowano, to {[7] } jest UTF-32.
  • _WIN32 - W Windows, wchar_t jest UTF-16, mimo że łamie to standard.
  • WCHAR_MAX - Może być użyty do określenia wielkości wchar_t, ale nie czy system operacyjny używa go do reprezentowania Unicode.

Czy to oznacza, że mój kod powinien nie używaj nigdzie typów znaków i to należy użyć funkcji, które mogą dogadać się z wint_t i wchar_t?

Zobacz też:

Nie. UTF-8 jest doskonale poprawnym kodowaniem Unicode, które używa ciągów char*. Ma tę zaletę, że jeśli twój program jest przejrzysty dla bajty inne niż ASCII (np. konwerter końców linii, który działa na \r i \n, ale przechodzi przez inne znaki bez zmian), nie musisz wprowadzać żadnych zmian!

Jeśli używasz UTF-8, musisz zmienić wszystkie założenia, że char = znak (np. nie wywołaj toupper W pętli) lub char = kolumna ekranowa (np. do owijania tekstu).

Jeśli wybierzesz UTF-32, będziesz miał prostotę znaków o stałej szerokości (ale nie grafemów o stałej szerokości }, ale będzie trzeba zmienić typ wszystkich ciągów).

Jeśli zastosujesz UTF-16, będziesz musiał odrzucić zarówno założenie znaków o stałej szerokości , jak i założenie 8-bitowych jednostek kodu, co sprawia, że jest to najtrudniejsza ścieżka aktualizacji z jednobajtowego kodowania.

Polecam aktywnie unikanie wchar_t ponieważ nie jest to cross-platform: czasami jest to UTF-32, czasami jest to UTF-16, a czasami jest to kodowanie wschodnioazjatyckie pre-Unicode. Polecam użycie typedefs

Co ważniejsze, unikaj TCHAR.

 3
Author: dan04,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:33

Zasadniczo chcesz radzić sobie z łańcuchami w pamięci jako tablicami wzar_t zamiast char. Podczas wykonywania dowolnego rodzaju operacji wejścia/wyjścia (np. odczytu / zapisu plików) można kodować / dekodować za pomocą UTF-8 (jest to prawdopodobnie najczęstsze kodowanie), co jest wystarczająco proste do zaimplementowania. Wystarczy wpisać w Google RFC. Tak więc w pamięci nic nie powinno być wielobajtowe. Jeden wzar_t reprezentuje jeden znak. Jeśli jednak przychodzisz do serializacji, wtedy musisz zakodować coś takiego jak UTF-8, gdzie niektóre znaki są reprezentowane przez wiele bajtów.

Będziesz musiał również napisać nowe wersje strcmp itp. dla szerokich ciągów znaków, ale to nie jest duży problem. Największym problemem będzie interap z bibliotekami / istniejącym kodem, które akceptują tylko tablice znaków.

A jeśli chodzi o sizeof (wzar_t) (będziesz potrzebował 4 bajtów, jeśli chcesz to zrobić dobrze) zawsze możesz zmienić jego definicję do większego rozmiaru za pomocą hacków typu/makro, jeśli zajdzie taka potrzeba.

 2
Author: Mike Weller,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-09 06:40:42

Nie ufałbym żadnej standardowej implementacji biblioteki. Wystarczy włączyć własne typy unicode.

#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;
}
 2
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-29 18:45:44

Z tego co wiem, wzar_t jest zależny od implementacji(jak widać z tego artykułu wiki). I to nie jest unicode.

 1
Author: PolyThinker,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-02-09 06:03:11