UTF-8 Kodlama Algoritması

RFC2231 Kodlaması başlıklı yazıda, wchar_t türündeki bir string'i utf8 olarak kodlamak için işletim sisteminin sağladığı fonksiyonları kullanmıştım. Ancak, daha sonra, kodlama egzersizi olarak wchar_t türündeki bir string'i utf8 olarak kodlayan fonksiyonu kendim yazmak istedim. Referans olarak, RFC 3629'daki tanımlamaları kullandım. Bu yazıda, geniş karakter (wchar_t) türündeki veriyi, nasıl utf8 olarak kodlayabileceğimizden bahsedeceğim.

Bahsettiğim RFC'nin 3. başlığı altında, UTF-8'in tanımı yapılmış. Buna göre, 0x0 ile 0x10FFFF arasındaki unicode karakterler, 1-4 byte dizilimle kodlanıyor. Kodlamada kullanılan her bir byte'a octet (eng. sekizli) deniyor. Tek bir octet'den oluşan dizilimde üst bit (MSB) 0 olmak zorunda, kalan 7 bit ile karakterimizi kodluyoruz. Dolayısıyla, karakter kodu 127 ve altı için, UTF8 kodlama ile us-ascii kodlama eşdeğer oluyor. n octetli'li (n > 1) dizilimlerde iste, ilk octet'in üst n biti 1 olarak, bunları takip eden ilk bit ise 0 olarak ayarlanıp, kalan bitler kodlama amaçlı kullanılıyor. Devamında gelen octetlerin ise, hepsinin en üst biti 1, onu takip eden bit ise 0 olarak ayarlanıp, kalan bitler kodlama amaçlı kullanılıyor. RFC'deki örnek tabloyu buraya kopyalıyorum.

   Karakter Kodu Aralığı  |        UTF-8 octet dizilimi
         (hexadecimal)    |              (binary)
      --------------------+---------------------------------------------
      0000 0000-0000 007F | 0xxxxxxx
      0000 0080-0000 07FF | 110xxxxx 10xxxxxx
      0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
      0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Şimdi adım adım, UTF8 kodlama algoritmasının aşamalarına geçeceğiz. Aşamalara geçmeden önce, işimizi daha anlaşılır yapabilmek için, tanımlamalarımızı yapalım.

#define UTF8_OCTET1_MAX 0x7F
#define UTF8_OCTET2_MAX 0x7FF
#define UTF8_OCTET3_MAX 0xFFFF
#define UTF8_OCTET4_MAX 0x10FFFF
#define UTF8_SEQUENCE1_START 0x00
#define UTF8_SEQUENCE2_START 0xC0
#define UTF8_SEQUENCE3_START 0xE0
#define UTF8_SEQUENCE4_START 0xF0
#define UTF8_CONTINUATION_BYTE 0x80
#define UTF8_CONTINUATION_BYTE_MASK 0x3F

İlk 4 tanım, sırasıyla 1 octet, 2 octet, 3 octet ve 4 octet ile kodlanabilecek karakter kodlarının üst sınırını belirtiyor. Bu sayılarını doğrudan örnek tablodan kopyaladım. Daha sonra gelen 4 tanım ise, yine sırasıyla 1 octet, 2 octet, 3 octet ve 4 octet dizilimlerinde ilk octet'in üst bitlerinde olması gereken değeri belirtiyor. Bu sayıları elde etmek için, örnek tabloda binary olarak gösterilen birinci octet'leri (b00000000,b11000000,b11100000,b11110000), hex gösterimine çevirdim. UTF8_CONTINUATION_BYTE tanımı, çoklu octet dizilimlerinde, her bir octet'in üst bitlerinin alması gerektiği değeri gösteriyor. Bunu da aynı şekilde örnek tablodan hex'e çevirdim. Son olarak, UTF8_CONTINUATION_BYTE_MASK ile tanımlanan değeri, bir sayının en alttaki 6 bitini maskelemek için kullanacağım.

Şimdi adım adım, algoritmayı inceleyelim. Birinci adım olarak, karakterin kaç octet ile kodlanacağını hesaplayacağız. Bunun için mümkün olan en düşük octet sayının kullanacağız.

// 1. Gerekli Octet Sayısını Hesapla
size_t cbOctet = 1  
               + (c > UTF8_OCTET1_MAX)
               + (c > UTF8_OCTET2_MAX)
               + (c > UTF8_OCTET3_MAX)
               + (c > UTF8_OCTET4_MAX);

Yukarıdaki örnekte, eğer UTF8_OCTET4_MAX'dan büyük bir karakter kodu ile karşılaşırsak, cbOctet 5 olacak. En fazla 4 octet kullanabileceğimiz için, bu durumu hata durumu olarak değerlendireceğiz. İkinci adımda ise, kullanılacak octet'lerin, üst bitleri tabloda gösterildiği şekilde ayarlayacağız.

// 2a. Başlangıç octet'inin üst bitlerini ayala
switch (cbOctet)
{
    case 1:
        result[0] = UTF8_SEQUENCE1_START;
        break;
    case 2:
        result[0] = UTF8_SEQUENCE2_START;
        break;
    case 3:
        result[0] = UTF8_SEQUENCE3_START;
        break;
    case 4:
        result[0] = UTF8_SEQUENCE4_START;
        break;
    default: // utf8 ile kodlanamayacak bir karakter varsa, 0 döndür
        return 0;
}
// 2b. devam octetlerinin üst bitlerini ayarla
size_t i;
for(i = 1; i < cbOctet; i++)
{
    result[i] = UTF8_CONTINUATION_BYTE;
}

İkinci adımda çok ilginç birşey yok. Her bir octet'in üst bitlerini olması gerektiği gibi ayarlayıp, kalan bitlerini sıfırladık. Üçüncü adımda, karakter kodundaki bitleri, octet'lerdeki kullanılabilir bitlere kopyalacağız. Bunun için, karakter kodunun en alttaki bitlerini en sondaki octet'e kopyalarak, yukarı doğru devam edeceğiz. Bunu yapmak için, en baştaki octet dışında, UTF8_CONTINUATION_BYTE_MASK maskesi ile alttaki 6 biti seçip, karakter kodunu 6 bit sağa kaydıracağız. En baştaki octet'e gelindiğinde, sadece ihtiyacımız olan bitler kaldığı için, herhangi bir maskeleme yapmamıza gerek yok.

// 3. Son octetten başa doğru, bitleri
// doldur. Her devam octet'inde 6'şar
// bit kullanacağız. Geriye kalan bitler
// teknik olarak başlangıç octet'ine
// sığmak zorunda
for(i = cbOctet; i > 1; i--)
{
    result[i-1] |= c & UTF8_CONTINUATION_BYTE_MASK;
    c = c >> 6;
}
result[0] |= c;

Böylece, tek bir unicode karakteri, utf8 olarak kodlamış olduk. Bu işlemi bir string üzerinde gerçekleştirmek için, sırayla tüm karakleri yukarıdaki şekilde kodlamak yeterli. Örnek programın son hali aşağıdaki gibi olacak.

#include <stdlib.h> // malloc
#include <string.h> // memcpy
#include <stdio.h>  // fopen, fwrite, fclose
#include <wchar.h>  // wchar_t
#define UTF8_OCTET1_MAX 0x7F
#define UTF8_OCTET2_MAX 0x7FF
#define UTF8_OCTET3_MAX 0xFFFF
#define UTF8_OCTET4_MAX 0x10FFFF
#define UTF8_SEQUENCE1_START 0x00
#define UTF8_SEQUENCE2_START 0xC0
#define UTF8_SEQUENCE3_START 0xE0
#define UTF8_SEQUENCE4_START 0xF0
#define UTF8_CONTINUATION_BYTE 0x80
#define UTF8_CONTINUATION_BYTE_MASK 0x3F
size_t utf8_encode_char(wchar_t c, unsigned char result[4])
{
    // 1. Gerekli Octet Sayısını Hesapla
    size_t cbOctet = 1  
                   + (c > UTF8_OCTET1_MAX)
                   + (c > UTF8_OCTET2_MAX)
                   + (c > UTF8_OCTET3_MAX)
                   + (c > UTF8_OCTET4_MAX);
    // 2a. Başlangıç octet'inin üst bitlerini ayala
    switch (cbOctet)
    {
        case 1:
            result[0] = UTF8_SEQUENCE1_START;
            break;
        case 2:
            result[0] = UTF8_SEQUENCE2_START;
            break;
        case 3:
            result[0] = UTF8_SEQUENCE3_START;
            break;
        case 4:
            result[0] = UTF8_SEQUENCE4_START;
            break;
        default: // utf8 ile kodlanamayacak bir karakter varsa, 0 döndür
            return 0;
    }
    // 2b. devam octetlerinin üst bitlerini ayarla
    size_t i;
    for(i = 1; i < cbOctet; i++)
    {
        result[i] = UTF8_CONTINUATION_BYTE;
    }
    // 3. Son octetten başa doğru, bitleri
    // doldur. Her devam octet'inde 6'şar
    // bit kullanacağız. Geriye kalan bitler
    // teknik olarak başlangıç octet'ine
    // sığmak zorunda
    for(i = cbOctet; i > 1; i--)
    {
        result[i-1] |= c & UTF8_CONTINUATION_BYTE_MASK;
        c = c >> 6;
    }
    result[0] |= c;
    return cbOctet;
}
/*
    IN: wchar_t *pwszWide: NULL ile biten, wchar_t stringi
    OUT: size_t *cbEncoded : Kodlamış string'in boyutu
    RETURN: utf8 ile kodlanmış, NULL ile biten array
*/
char *utf8_encode(wchar_t *pwszWide, size_t *cbEncoded)
{
    *cbEncoded = 0;
    size_t cbTemp;
    unsigned char encoded_char[4];
    wchar_t *pwcTemp;
    char    *pcTemp2;
    // realloc yapmak zorunda kalmamak için
    // pwszWide üzerinden 1 tur geçip, kodlanmış
    // veri için gerekli hafızayı hesaplayacağız.
    for(pwcTemp = pwszWide; *pwcTemp; pwcTemp++)
    {
        *cbEncoded += utf8_encode_char(*pwcTemp, encoded_char);
    }
    char *pszEncoded = malloc(*cbEncoded + 1); // +1 null
    for(pwcTemp = pwszWide, pcTemp2 = pszEncoded; *pwcTemp; pwcTemp++)
    {
        cbTemp = utf8_encode_char(*pwcTemp, encoded_char);
        memcpy(pcTemp2, encoded_char, cbTemp);
        pcTemp2 += cbTemp;
    }
    *pcTemp2 = 0; // NULL terminator
    return pszEncoded;
}
int main(void)
{
    wchar_t example1[] = L"İŞTE BUNLAR HEP TÜRKÇE KARAKTERLER: ÜĞİŞÇÖ";
    wchar_t example2[] = {0xD55C, 0xAD6D, 0xC5B4, 0x0}; // korece korece
    wchar_t example3[] = {0x65E5, 0x672C, 0x8A9E, 0x0}; // japonca japon
    wchar_t example4[] = {0x233B4, 0x0};                // çince bir karater
    FILE *f = fopen("examples.txt","wb");
    size_t cbEncoded;
    char *pszEncoded = utf8_encode(example1, &cbEncoded);
    fprintf(f, "%s\n", pszEncoded);
    free(pszEncoded);
    pszEncoded = utf8_encode(example2, &cbEncoded);
    fprintf(f, "%s\n", pszEncoded);
    free(pszEncoded);
    pszEncoded = utf8_encode(example3, &cbEncoded);
    fprintf(f, "%s\n", pszEncoded);
    free(pszEncoded);
    pszEncoded = utf8_encode(example4, &cbEncoded);
    fprintf(f, "%s\n", pszEncoded);
    free(pszEncoded);
    fclose(f);
}

Öyle sanıyorum ki, yukarıdaki kodlarda optimize edilebilecek kısımlar vardır. Ancak, kolayca anlaşılabilr olması açısından, yapılabilecek optimizasyonlara dikkat etmedim. Faydalı bir yazı olmuştur diye ümit ediyorum.