Programowanie w C: Jak programować dla Unicode?

83

Jakie wymagania wstępne są potrzebne do ścisłego programowania w standardzie Unicode?

Czy to oznacza, że ​​mój kod nie powinien charnigdzie używać typów i że muszą być używane funkcje, które mogą obsługiwać wint_ti wchar_t?

A jaka jest rola wielobajtowych sekwencji znaków w tym scenariuszu?

prinzdezibel
źródło

Odpowiedzi:

21

Zauważ, że nie chodzi o „ścisłe programowanie Unicode” jako takie, ale o pewne praktyczne doświadczenie.

W mojej firmie utworzyliśmy bibliotekę opakowującą wokół biblioteki ICU IBM. Biblioteka opakowująca ma interfejs UTF-8 i konwertuje do UTF-16, gdy konieczne jest wywołanie ICU. W naszym przypadku nie przejmowaliśmy się zbytnio hitami wydajnościowymi. Gdy problemem była wydajność, dostarczyliśmy również interfejsy UTF-16 (używając naszego własnego typu danych).

Aplikacje mogą pozostać w dużej mierze takie, jakie są (przy użyciu znaku), chociaż w niektórych przypadkach muszą być świadome pewnych problemów. Na przykład zamiast strncpy () używamy opakowania, które pozwala uniknąć odcinania sekwencji UTF-8. W naszym przypadku jest to wystarczające, ale można by też rozważyć sprawdzanie łączenia znaków. Mamy też opakowania do zliczania liczby punktów kodowych, liczby grafemów itp.

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

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

Hans van Eck
źródło
2
Prawidłowa sekwencja bajtów UTF-8 nigdy nie zostanie odcięta (obcięta) przez strncpy. Prawidłowe sekwencje UTF-8 nie mogą zawierać żadnych bajtów 0x00 (poza oczywiście kończącym bajtem zerowym).
Dan Molding
8
@Dan Molding: jeśli strncpy (), powiedzmy, ciąg zawierający pojedynczy znak chiński (który może mieć 3 bajty) w 2-bajtowej tablicy znaków, utworzysz nieprawidłową sekwencję UTF-8.
Hans van Eck
@Hans van Eck: Jeśli twój wrapper kopiuje ten pojedynczy 3-bajtowy chiński znak do 2-bajtowej tablicy, to albo skracasz go i tworzysz nieprawidłową sekwencję, albo będziesz miał niezdefiniowane zachowanie. Oczywiście, jeśli kopiujesz dane, cel musi być wystarczająco duży; to rzecz zupełnie zrozumiała. Chodziło mi o to, że strncpyprawidłowo używany jest całkowicie bezpieczny w użyciu z UTF-8.
Dan Molding
5
@DanMoulding: Jeśli wiesz, że twój docelowy bufor jest wystarczająco duży, możesz po prostu użyć strcpy(co jest rzeczywiście bezpieczne w użyciu z UTF-8). Ludzie używający strncpyprawdopodobnie robią to, ponieważ nie wiedzą, czy bufor docelowy jest wystarczająco duży, więc chcą przekazać maksymalną liczbę bajtów do skopiowania - co może rzeczywiście spowodować powstanie nieprawidłowych sekwencji UTF-8.
Frerich Raabe
42

C99 lub wcześniej

Standard C (C99) zapewnia szerokie znaki i znaki wielobajtowe, ale ponieważ nie ma gwarancji, co te szerokie znaki mogą pomieścić, ich wartość jest nieco ograniczona. Dla danej implementacji zapewniają przydatne wsparcie, ale jeśli Twój kod musi mieć możliwość przemieszczania się między implementacjami, to nie ma wystarczającej gwarancji, że będą przydatne.

W związku z tym podejście zaproponowane przez Hansa van Ecka (polegające na napisaniu otoki wokół biblioteki ICU - International Components for Unicode) jest rozsądne, IMO.

Kodowanie UTF-8 ma wiele zalet, z których jedną jest to, że jeśli nie zepsujesz danych (na przykład skracając je), mogą zostać skopiowane przez funkcje, które nie są w pełni świadome zawiłości UTF-8 kodowanie. To zdecydowanie nie dotyczy wchar_t.

Pełny Unicode to format 21-bitowy. Oznacza to, że Unicode rezerwuje punkty kodowe od U + 0000 do U + 10FFFF.

Jedną z przydatnych rzeczy w formatach UTF-8, UTF-16 i UTF-32 (gdzie UTF oznacza format transformacji Unicode - patrz Unicode ) jest to, że można konwertować między tymi 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.

Wiadomo, że UTF-8 jest formatem wielobajtowym, o starannej strukturze, która umożliwia niezawodne znajdowanie początku znaków w ciągu, począwszy od dowolnego punktu ciągu. Znaki jednobajtowe mają ustawiony wysoki bit na zero. Znaki wielobajtowe mają pierwszy znak zaczynający się od jednego ze wzorów bitowych 110, 1110 lub 11110 (dla znaków 2-bajtowych, 3-bajtowych lub 4-bajtowych), a kolejne bajty zawsze zaczynają się od 10. Znaki kontynuacji są zawsze w zakres 0x80 .. 0xBF. Istnieją zasady, zgodnie z którymi znaki UTF-8 muszą być przedstawiane w jak najmniejszym formacie. Jedną z konsekwencji tych reguł jest to, że bajty 0xC0 i 0xC1 (także 0xF5..0xFF) nie mogą występować w prawidłowych danych UTF-8.

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

UTF-16 jest zatem pojedynczym kodem jednostki (słowo 16-bitowe) dla „Basic Multilingual Plane”, co oznacza znaki z punktami kodowymi Unicode U + 0000 .. U + FFFF, ale wykorzystuje dwie jednostki (32-bitowe) dla znaków spoza tego zakresu. Tak więc kod, który działa z kodowaniem UTF-16, musi być w stanie obsługiwać kodowanie o zmiennej szerokości, tak jak musi to być UTF-8. Kody znaków podwójnych nazywane są surogatami.

Surogaty to punkty kodowe z dwóch specjalnych zakresów wartości Unicode, zarezerwowanych do użytku jako wartości wiodące i końcowe sparowanych jednostek kodu w UTF-16. Wiodące, zwane również wysokimi, surogaty są od U + D800 do U + DBFF, a końcowe lub niskie, zastępcze są od U + DC00 do U + DFFF. Nazywa się je surogatami, ponieważ nie reprezentują postaci bezpośrednio, ale tylko jako parę.

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

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

C11 i <uchar.h>

Standard C11 zmienił zasady, ale nie wszystkie implementacje nadążyły za zmianami nawet teraz (połowa 2017 roku). Standard C11 podsumowuje zmiany dotyczące obsługi Unicode jako:

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

Poniżej przedstawiono minimalny zarys funkcjonalności. Specyfikacja obejmuje:

6.4.3 Uniwersalne nazwy znaków

Składnia
nazwa-znaku-uniwersalnego:
    \u quad-quad
    \U hex-quad hex-quad
hex-quad:
    cyfra-szesnastkowa cyfra-szesnastkowa cyfra-szesnastkowa

7.28 Narzędzia Unicode <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);

który jest typem liczby całkowitej bez znaku używanej dla znaków 16-bitowych i jest tego samego typu, co uint_least16_t(opisany w 7.20.1.2); i

który jest typem liczby całkowitej bez znaku używanej dla znaków 32-bitowych i jest tego samego typu, co uint_least32_t(również opisany w 7.20.1.2).

(Tłumaczenie odsyłaczy: <stddef.h>definiuje size_t, <wchar.h>definiuje mbstate_ti <stdint.h>definiuje uint_least16_ti uint_least32_t.) <uchar.h>Nagłówek definiuje również minimalny zestaw (uruchamialnych) funkcji konwersji:

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

Istnieją reguły określające, które znaki Unicode mogą być używane w identyfikatorach przy użyciu notacji \unnnnlub \U00nnnnnn. Może być konieczne aktywne aktywowanie obsługi takich znaków w identyfikatorach. Na przykład GCC wymaga, -fextended-identifiersaby zezwolić na te identyfikatory.

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

Jonathan Leffler
źródło
3
Myślę, że wchar_ttrochę tu sprzedajesz i przyjaciół. Te typy są niezbędne, aby umożliwić bibliotece C obsługę tekstu w dowolnym kodowaniu (w tym kodowaniu innym niż Unicode). Bez szerokich typów znaków i funkcji biblioteka C wymagałaby zestawu funkcji obsługujących tekst dla każdego obsługiwanego kodowania: wyobraź sobie, że koi8len, koi8tok, koi8printf tylko dla tekstu zakodowanego KOI-8 i utf8len, utf8tok, utf8printf dla UTF-8 tekst. Zamiast tego, mamy szczęście mieć tylko jeden zestaw tych funkcji (nie licząc oryginalne ASCII) wcslen, wcstokoraz wprintf.
Dan Molding
1
Wszystko, co musi zrobić programista, to użyć funkcji konwersji znaków z biblioteki C ( mbstowcsi przyjaciół), aby przekonwertować dowolne obsługiwane kodowanie na wchar_t. Po wchar_tsformatowaniu programista może używać pojedynczego zestawu funkcji obsługi szerokiego tekstu, które zapewnia biblioteka C. Dobra implementacja biblioteki C obsługuje praktycznie każde kodowanie, którego większość programistów kiedykolwiek będzie potrzebować (na jednym z moich systemów mam dostęp do 221 unikalnych kodowań).
Dan Molding
Jeśli chodzi o to, czy będą wystarczająco szerokie, aby były użyteczne: norma wymaga, aby implementacja wchar_tbyła wystarczająco szeroka, aby pomieścić dowolny znak obsługiwany przez implementację. Oznacza to (z prawdopodobnie jednym godnym uwagi wyjątkiem) większość implementacji zapewni, że są one na tyle szerokie, że program, który używa wchar_t, poradzi sobie z każdym kodowaniem obsługiwanym przez system (Microsoft wchar_tma tylko 16-bitową szerokość, co oznacza, że ​​ich implementacja nie obsługuje w pełni wszystkich kodowań, przede wszystkim różne kodowania UTF, ale ich jest wyjątkiem, a nie regułą).
Dan Molding
11

To często zadawane pytania zawiera wiele informacji. Pomię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_tto 16 bitów w systemie Windows, ale niekoniecznie 16 bitów na innych platformach. Myślę, że jest to zło konieczne w systemie Windows, ale prawdopodobnie można go uniknąć gdzie indziej. Powodem, dla którego jest to ważne w systemie Windows, jest to, że potrzebujesz go do używania plików, które mają w nazwie znaki inne niż ASCII (wraz z wersją funkcji W).

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

  • Jeśli jesteś programowania na Windows, nie można używać fopen(), fread(), fwrite(), itd., Ponieważ tylko brać char *i nie rozumieją kodowanie UTF-8. Sprawia, że ​​przenoszenie jest bolesne.

dbyron
źródło
Zauważ, że stdio f*i znajomych z pracy char *na każdej platformie, ponieważ standard mówi tak - użyj wcs*zamiast do wchar_t.
kot
7

Aby wykonać ścisłe programowanie w Unicode:

  • Używać tylko interfejsów API ciąg Unicode, które są świadome ( NIE strlen , strcpy... ale ich odpowiedniki WideString wstrlen, wsstrcpy...)
  • Kiedy mamy do czynienia z blokiem tekstu, należy stosować kodowanie, które umożliwia 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żywaj czcionek zgodnych z Unicode (np. Arial_unicode)

Wielobajtowe sekwencje znaków to kodowanie poprzedzające 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 wint_t.

sebastien
źródło
wint_t jest typem zdefiniowanym w <wchar.h>, podobnie jak wchar_t. Ma taką samą rolę w odniesieniu do szerokich znaków, jak int w odniesieniu do „char”; może zawierać dowolną wartość znaku szerokiego lub WEOF.
Jonathan Leffler
3

Najważniejsze jest, aby zawsze wyraźnie odróżniać tekst od danych binarnych . Starają się podążać za model Python 3.x strvs.bytes lub SQL TEXTvs. BLOB.

Niestety, C myli problem, używając charzarówno dla „znaku ASCII”, jak i int_least8_t. Będziesz chciał zrobić coś takiego:

Możesz również potrzebować czcionek typu dla jednostek kodu UTF-16 i UTF-32, ale jest to bardziej skomplikowane, ponieważ kodowanie wchar_tnie jest zdefiniowane. Będziesz potrzebował tylko preprocesora #if. Niektóre przydatne makra w C i C ++ 0x to:

  • __STDC_UTF_16__- Jeśli zdefiniowano, typ _Char16_tistnieje i to UTF-16.
  • __STDC_UTF_32__- Jeśli zdefiniowano, typ _Char32_tistnieje i to UTF-32.
  • __STDC_ISO_10646__- Jeśli zdefiniowano, to wchar_tjest to UTF-32.
  • _WIN32- W systemie Windows wchar_tjest UTF-16, mimo że łamie to standard.
  • WCHAR_MAX- Może być używany do określenia rozmiaru wchar_t, ale nie do określenia , czy system operacyjny używa go do reprezentowania Unicode.

Czy to oznacza, że ​​mój kod nie powinien nigdzie używać typów char i że muszą być używane funkcje, które radzą sobie z wint_t i wchar_t?

Zobacz też:

Nie. UTF-8 to doskonale poprawne kodowanie Unicode, które używa char*ciągów. Ma tę zaletę, że jeśli twój program jest przezroczysty dla bajtów spoza ASCII (np. Konwerter kończący linię, który działa na innych znakach \ri \nprzepuszcza je bez zmian), nie będziesz musiał dokonywać żadnych zmian!

Jeśli wybierzesz UTF-8, będziesz musiał zmienić wszystkie założenia, że char= znak (np. Nie wywołuj toupperw pętli) lub char= kolumna ekranu (np. Do zawijania tekstu).

Jeśli zdecydujesz się na UTF-32, będziesz miał prostotę znaków o stałej szerokości (ale nie grafemów o stałej szerokości , ale będziesz musiał zmienić typ wszystkich swoich ciągów).

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

Zalecałbym aktywne unikanie, wchar_t ponieważ nie jest to wieloplatformowe: czasami jest to UTF-32, czasami jest to UTF-16, a czasami jest to kodowanie wschodnioazjatyckie poprzedzające Unicode. Polecam używanietypedefs

Co ważniejsze, unikajTCHAR .

dan04
źródło
Nie sądzę, żeby to w ogóle było niefortunne - char jest int. To jest korzyść. Użycie literalnych stałych znakowych przychodzi na myśl jako jedno użycie. A funkcje, które przyjmują, char *mogą mieć problemy, jeśli zostaną przekazane jako const char *ostatnie, o których pamiętam (ale nie jestem dokładny w tym i które funkcje, więc weź to ze szczyptą soli). To, że jest to bardziej skomplikowane w przypadku innych języków, nie oznacza, że ​​jest to zły projekt.
Pryftan
2

Nie ufałbym żadnej standardowej implementacji biblioteki. Po prostu rzuć własne typy Unicode.


źródło
2

Zasadniczo chcesz traktować łańcuchy w pamięci jako wchar_ttablice zamiast znaków . Kiedy wykonujesz jakiekolwiek operacje we / wy (takie jak czytanie / zapisywanie plików), możesz kodować / dekodować przy użyciu UTF-8 (jest to prawdopodobnie najpopularniejsze kodowanie), które jest wystarczająco proste do zaimplementowania. Po prostu wygoogluj RFC. Więc nic w pamięci nie powinno być wielobajtowe. Jeden wchar_treprezentuje jedną postać. Jednak kiedy przychodzi do serializacji, wtedy musisz zakodować do czegoś takiego jak UTF-8, gdzie niektóre znaki są reprezentowane przez wiele bajtów.

Będziesz także musiał napisać nowe wersje strcmpitp. Dla szerokich ciągów znaków, ale nie jest to duży problem. Największym problemem będzie współdziałanie z bibliotekami / istniejącym kodem, który akceptuje tylko tablice char.

A jeśli chodzi o sizeof(wchar_t)(będziesz potrzebować 4 bajtów, jeśli chcesz to zrobić dobrze), zawsze możesz przedefiniować go do większego rozmiaru za pomocą typedef/ macrohacks, jeśli chcesz.

Mike Weller
źródło
1

Z tego co wiem, wchar_t jest zależny od implementacji (jak widać w tym artykule wiki ). I to nie jest Unicode.

PolyThinker
źródło