Egy hálózatról érkező adatfolyam oktettekből áll; attól válik
szöveggé, hogy egy kódolási szerződés szerint értelmezzük. A C
char típusa önmagában nem fejez ki ilyen szerződést:
tárolhat szövegrészletet, terminátort vagy bináris adatot is.
A Nullterminált sztring és a hossz szerződése rész alapján már tudjuk, hogy a nullterminált sztring és a pointer–hossz pár eltérő interfész. Most ehhez hozzáadjuk a kódolást: egy tartomány lehet memóriahatár szempontjából érvényes, miközben hibás UTF-8-at tartalmaz.
Az Egész típusok és adatmodellek jelölését
követve egy C bájt CHAR_BIT bitből áll. Az
oktett pontosan nyolcbites egység. Az UTF-8
oktettsorozatokkal definiált kódolás; a fejezet konkrét példáiban ezért
CHAR_BIT == 8 az előfeltétel.
A Unicode-kódpont egy azonosító a
U+0000–U+10FFFF tartományban. A
Unicode skalárérték olyan kódpont, amely nem tartozik a
U+D800–U+DFFF surrogate tartományba. Érvényes
UTF-8 csak skalárértékeket kódol.
A felhasználó által egyetlen karakternek érzékelt egységet gyakran grafémaklaszterként (grapheme cluster) közelítjük. Ez több kódpontból is állhat, és nem azonos sem a kirajzolt glyph-ek számával, sem a termináloszlopszélességgel.
| Szöveg | UTF-8 oktettek hexadecimálisan | Kódpontok száma | Grafémaklaszterek száma |
|---|---|---|---|
A |
41 |
1 | 1 |
á |
C3 A1 |
1 | 1 |
a és utána kombináló ékezet |
61 CC 81 |
2 | 1 |
Az utolsó két sor megjelenése lehet azonos, a bájtsorozat mégsem az. A bájtonkénti összehasonlítás ezért nem általános nyelvi szöveg-összehasonlítás. A normalizálás és a grafémák teljes kezelése külön Unicode-feldolgozást igényel.
ISO C17: a forrásfájl fizikai kódolása és a
közönséges sztringliterálok végrehajtási kódolása (execution
encoding) külön kérdés. Egy UTF-8-ban mentett forrásfájl önmagában
nem garantálja, hogy a benne szereplő "á" literál UTF-8
oktetteket eredményez.
A "\u00E1" univerzális karakterneve egyértelműsíti a
szándékolt karaktert, de a közönséges literál továbbra is a végrehajtási
karakterkészlet szabályait követi. Ezzel szemben a C17
u8"\u00E1" UTF-8 sztringliterál; nyolcbites C bájt mellett
tartalma C3 A1 00.
C17-ben az ilyen literál is char elemtípusú tömb. A
típus tehát nem különbözteti meg az UTF-8 szöveget más char
adatoktól. A kódolás az API dokumentált szerződésének része marad.
char,
karakterosztályozás és locale<ctype.h> előfeltételeISO C17: a sima char előjelessége
implementációfüggő. Ha előjeles, egy UTF-8 oktett char
értékként negatív lehet.
Az isspace, isalpha, tolower
és a többi <ctype.h> függvény argumentuma csak:
unsigned char típussal ábrázolható érték; vagyEOF értékelehet. Más argumentum nem definiált viselkedést okoz.
Egy érvényes text[i] tömbelem osztályozásának megfelelő
alakja ezért isspace((unsigned char)text[i]). A konverzió
helyreállítja a bájt nemnegatív értékét az egészpromóció előtt.
Ez nem Unicode-dekódolás. Egy többoktettes UTF-8
kódolás minden oktettjére külön meghívott isalpha nem dönti
el, hogy a kódpont betű-e; a bájtonkénti tolower pedig nem
általános Unicode kisbetűsítés.
Az EOF külön figyelmet igényel: például az
fgetc eredményét először int változóban kell
megvizsgálni. Az unsigned char konverzió után már nem
különböztethető meg megbízhatóan a fájlvég egy adatbájttól.
LC_CTYPE
globális szerződéseISO C17: a <locale.h>
setlocale függvénye módosítja a program locale-beállítását.
Az LC_CTYPE kategória többek között a karakterosztályozásra
és a multibájtos konverziókra hat.
Induláskor a szabványos környezet a "C" locale-nak felel
meg. A setlocale(LC_CTYPE, "") a környezet által
meghatározott beállítást kéri; sikertelenségkor null pointert ad
vissza.
A siker nem bizonyítja, hogy a kiválasztott kódolás
UTF-8. Az olyan nevek, mint a "C.UTF-8", nem ISO C által
garantált locale-nevek; elérhetőségük az adott libc-től és telepítéstől
függ.
A globális állapot problémája szolgáltatásokban különösen fontos. Egy könyvtár locale-váltása más komponensek értelmezését is megváltoztathatja. Jó alapelv, hogy az alkalmazás inicializáláskor állítja be a szükséges locale-t, és feldolgozás közben nem változtatja. Egy rögzítetten UTF-8 hálózati protokollhoz gyakran jobb locale-független dekódert választani.
wchar_t nem
univerzális Unicode-típusISO C17: a wchar_t széles karakterek
tárolására szolgáló egész típus, de mérete és értelmezése
implementációfüggő. Sem a neve, sem a sizeof(wchar_t)
értéke nem bizonyítja, hogy minden eleme egy Unicode-kódpont.
Linux/glibc környezetben gyakori a 32 bites, Unicode-hoz illeszkedő megvalósítás, de ez implementációs tulajdonság, nem hordozható nyelvi garancia.
| Header | Konverziós függvények | Kimeneti/bemeneti egység |
|---|---|---|
<wchar.h> |
mbrtowc, wcrtomb |
wchar_t |
<uchar.h> |
mbrtoc16, c16rtomb |
char16_t |
<uchar.h> |
mbrtoc32, c32rtomb |
char32_t |
A multibájtos oldal ezeknél is az aktuális LC_CTYPE
szerinti kódolás. A mbrtoc32 tehát nem automatikusan
„UTF-8-dekóder”.
C17-ben a char16_t és char32_t
Unicode-értelmezéséhez is ellenőrizni kell az implementáció garanciáit;
erre szolgálnak például a __STDC_UTF_16__ és
__STDC_UTF_32__ makrók. UTF-16 esetén egy nem BMP-beli
skalárérték két kódegységet igényel.
mbstate_t a
feldolgozás részeAz újraindítható konverziók konverziós állapotot
tárolnak egy mbstate_t objektumban. A
mbstate_t state = {0}; kezdeti konverziós állapotot hoz
létre.
Az állapotnak túl kell élnie az összetartozó bemeneti darabok feldolgozását. Külön adatfolyamokhoz külön állapot tartozik. Null állapotpointer helyett célszerű saját objektumot átadni, így nem támaszkodunk a függvény belső, megosztott állapotára.
A bemeneti konverziók fontos eredményei:
| Eredmény | Jelentés és teendő |
|---|---|
| Pozitív hossz | Siker; ennyi C bájtot fogyasztott az aktuális bemenetből |
0 |
Null karakter; UTF-8 esetén egy oktettet fogyasztott |
(size_t)-2 |
Hiányos szekvencia; az állapot már rögzíti a feldolgozott részt |
(size_t)-1 |
Kódolási hiba; az állapot nem használható tovább megalapozatlanul |
(size_t)-3 |
Az ezt támogató konverziónál további kimeneti egység, új bemenet fogyasztása nélkül |
Az utolsó eset például mbrtoc16 esetén fontos: egy
UTF-16 surrogate pár második egysége későbbi hívásban jelenhet meg.
Kidolgozott lépés: UTF-8 locale mellett a
C3 A1 szekvenciát két darabban kapjuk. Az első, egyoktettes
mbrtoc32 hívás eredménye (size_t)-2.
Ugyanazzal az állapottal ezután csak az új A1 oktettet
adjuk át. A C3 ismételt átadása megkettőzné a már
feldolgozott bemenetet.
Hibánál az alkalmazásnak kell döntenie elutasításról vagy helyreállításról. Az állapot nullázása önmagában nem határozza meg, melyik bemeneti pozíción biztonságos folytatni.
Az UTF-8 önszinkronizáló, változó hosszúságú kódolás: egy skalárérték egy–négy oktett. Nem elég azonban a kezdőbájt alapján megszámolni a folytatóbájtokat. El kell utasítani:
U+10FFFF fölötti értékeket.Az alábbi program egyetlen skalárérték kódolását ellenőrzi, majd végigjár egy explicit hosszúságú puffert.
Fordítási profil: ISO C17, GNU bővítések nélkül; választott laborfordító GCC 14.2, Linux x86-64 System V, LP64, glibc 2.40. A kód nem függ a locale-tól; a nyolcbites C bájtot fordításkor ellenőrzi. Az alábbi eredmények elvárások, nem dokumentált futtatási eredmények.
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
_Static_assert(CHAR_BIT == 8, "Eight-bit bytes required");
/* 1: valid unit; 0: incomplete; -1: invalid.
*used changes only on success. */
static int utf8_unit(const unsigned char *p, size_t n,
size_t *used)
{
if (n == 0)
return 0;
unsigned char b = p[0];
size_t k;
if (b <= 0x7F) {
*used = 1;
return 1;
}
if (b >= 0xC2 && b <= 0xDF)
k = 2;
else if (b >= 0xE0 && b <= 0xEF)
k = 3;
else if (b >= 0xF0 && b <= 0xF4)
k = 4;
else
return -1;
for (size_t i = 1; i < k; ++i) {
if (i == n)
return 0;
unsigned char c = p[i];
if (c < 0x80 || c > 0xBF)
return -1;
if (i == 1 &&
((b == 0xE0 && c < 0xA0) ||
(b == 0xED && c > 0x9F) ||
(b == 0xF0 && c < 0x90) ||
(b == 0xF4 && c > 0x8F)))
return -1;
}
*used = k;
return 1;
}
int main(void)
{
const unsigned char data[] = {
0x41, 0xC3, 0xA1, 0x00, 0x42
};
size_t offset = 0;
size_t count = 0;
while (offset < sizeof data) {
size_t used = 0;
int status = utf8_unit(data + offset,
sizeof data - offset, &used);
if (status != 1) {
fprintf(stderr, "UTF-8 error at %zu: %d\n",
offset, status);
return 1;
}
offset += used;
++count;
}
printf("octets=%zu, scalar_values=%zu\n",
sizeof data, count);
return 0;
}Fordítás a külön tanulói laborban:
gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
-O2 utf8_units.c -o utf8_units| Szerep | Szerződés |
|---|---|
| Tulajdonos és tároló | A main automatikus data tömbje |
| Kölcsönzött nézet | A p, n pár; a függvény nem tárolja el |
| Előfeltétel | n > 0 esetén p legalább n
olvasható C bájtot jelöl |
| Eredménytároló | used élő, írható size_t objektumra mutat,
a bemeneten kívül |
| Érvénytelenítés | A tulajdonos blokkjának elhagyása; közben nincs tárolócsere |
| Hibakimenet | Hiányos vagy hibás kódolásnál nincs előrelépés és nincs új
used érték |
A várt számlálás öt oktett és négy skalárérték. A beágyazott
00 érvényes UTF-8, az U+0000 kódolása.
Nullterminált sztringként ugyanez a tartalom az első három C bájt után
véget érne.
Bizonyításvázlat: az ASCII-ág egyetlen oktettet
fogad el. A további ágak kizárják az illegális kezdőértékeket,
ellenőrzik a folytatóoktetteket, majd a második oktett speciális
korlátaival kizárják az overlong alakokat, surrogate értékeket és a
felső határ átlépését. A p[i] olvasását mindig megelőzi a
hosszellenőrzés.
A teljes bejárás idejű, kiegészítő tárolót használ, nem allokál, és sorfolytonosan olvas. A segédfüggvény hívásonként legfeljebb négy oktettet vizsgál.
A C3 A1 két oktettje között elvágott szöveg első része
hiányos UTF-8. Hálózati olvasás végén ez még nem feltétlenül hiba: a
következő olvasás hozhatja a folytatást. A logikai üzenet végén viszont
már elutasítandó.
A segédfüggvény nem őriz állapotot. Hiányos eredménynél ezért a
hívónak kell megtartania a teljes befejezetlen szekvenciát. Ez
szándékosan eltér az mbstate_t-alapú konverzió
szerződésétől.
A skalárérték-határon végzett vágás érvényes UTF-8-at hagyhat, mégis szétszakíthat egy grafémaklasztert. A kódolási érvényesség és a megjelenítési helyesség két külön követelmény.
Értelmezés: a 61 CC 81 00 42
sorozat hány oktett, skalárérték és első null előtti C bájt? Hány
grafémaklaszterből áll a null előtti szöveg?
Kódolás: a fenti segédfüggvénnyel készíts
utf8_prefix függvényt, amely előzetesen teljes egészében
validált bemenetből megadja a legfeljebb limit oktettes,
skalárérték-határon végződő prefix hosszát. Ne másoljon és ne
allokáljon.
Diagnosztika: UTF-8 locale mellett egy program a
C3 oktettre (size_t)-2 eredményt kap.
Ugyanazzal az állapottal ezután a teljes C3 A1 sorozatot
adja át újra. Emellett minden bemeneti char értékre
közvetlenül isalpha-t hív. Mi a két külön hiba?
Öt oktett és négy skalárérték található a sorozatban:
U+0061, U+0301, U+0000,
U+0042. Az első null előtti hossz három C bájt. Az
a és a kombináló ékezet együtt egy grafémaklaszter.
Ellenőrzési feltétel: a bájthossz nem számolható a megjelenített karakterek megszámlálásával.
C17-részlet, a fenti utf8_unit definíciója után
illeszthető:
static size_t utf8_prefix(const unsigned char *p, size_t n,
size_t limit)
{
size_t offset = 0;
if (limit > n)
limit = n;
while (offset < limit) {
size_t used = 0;
if (utf8_unit(p + offset, n - offset, &used) != 1)
break;
if (used > limit - offset)
break;
offset += used;
}
return offset;
}A bemeneti tároló tulajdonosa a hívó; a függvény nem tart meg pointert. Az előzetes validálás a szerződés része: az eredmény önmagában nem jelzi, hogy a kihagyott utótag hibás-e.
A 41 C3 A1 42 bemeneten a 2 oktettes korlát eredménye 1,
a 3 oktettesé 3. A limit - offset kivonás biztonságos, mert
a ciklusban offset < limit.
Az első hiba a bemenet megkettőzése: az állapot már feldolgozta a
C3 oktettet, ezért csak az új A1 oktettet kell
átadni.
A második az isalpha argumentumtartománya: negatív,
EOF-tól különböző char érték átadása UB. Az
unsigned char konverzió ezt javítja, de
Unicode-betűosztályozást nem valósít meg.
Ellenőrzési feltétel: próbáljuk a szekvenciát minden oktetthatáron darabolni, és külön vizsgáljuk a karakterosztályozás típushelyességét.
A szövegkezelési szerződésnek legalább a tároló élettartamát, a
bájthosszt, a kódolást és a hibapolitikát rögzítenie kell. A
char nem graféma, a wchar_t nem hordozható
Unicode-kódponttípus, a locale pedig rejtett globális bemenet lehet.
Hálózati UTF-8 feldolgozásakor a memóriahatár, a skalárértékhatár és a
felhasználó által érzékelt karakterhatár külön ellenőrzési szint.