JUNのブログ

JUNのブログ

活動記録や技術メモ

C言語でわかる? UTF-8

そういえばC言語でマルチバイト文字(UTF-8)の出力ってどうやってするんだろうと思って, 色々試したのでここに書き残しておく.

ちなみに今回は基本的に画面出力にはUNIXシステムコールを呼び出す write() を使う. write()に関しはmanコマンドでマニュアルを見るか以下のページを見るなりして適当に雰囲気を掴んでくれ.

miyanetdev.com

では本題のUTF-8について書いていくぞ

UTF-8 について

UTF-8はみんな普段から使っていると思うが, じゃあUTF-8がどういう風にデータで表現されてるか知ってるか? 俺は知らんかった.

ということでWikipedia見てみた.

ja.wikipedia.org

1バイト目の先頭の連続するビット "1"(その後にビット "0" が1つ付く)の個数で、その文字のバイト数がわかるようになっている。また、2バイト目以降はビットパターン "10" で始まり、1バイト目と2バイト目以降では値の範囲が重ならないので、文字境界を確実に判定できる。すなわち、任意のバイトの先頭ビットが "0" なら1バイト文字、"10" なら2バイト以上の文字の2番目以降のバイト、"110" なら2バイト文字の先頭バイト、"1110" なら3バイト文字の先頭バイト、"11110" なら4バイト文字の先頭バイトであると判定できる。

ということらしい.

こういうのは例があるとわかりやすいので日本語ひらがなのを例にすると

まず, UTF-8では次のように表される (わかりやすいように8bit(1Byte)ごとに:で区切っている)

11100011:10000001:10000010

さっきのWikipediaを見ながら読み解いていくと まず先頭バイトである1バイト目の最初に1が3つ続いているのでUTF-8で表現するには3バイト必要なことがわかる.

ちなみに1バイト目全部がデータのバイト数表すわけではなく,先頭4bitがデータ長を表し,残りは対象の文字を表現するのに使うらしい...?

また、5-6バイトの表現は、ISO/IEC 10646による定義[4]とIETFによるかつての定義[5]で、Unicodeの範囲外を符号化するためにのみ使用するが、Unicodeによる定義[6]とIETFによる最新の定義[7]では、5-6バイトの表現は不正なシーケンスである。

一応4バイト以上の表現も可能ではあるが基本的に不正なシーケンスらしいので今回は考えなくて良さそう.

で, の話に戻ると, 先頭4bitがデータサイズを表していることがわかったので,残りは 1バイト目の後半4bitとその後ろの16bitを読めばいいらしい.

Wikipediaに載ってる表がわかりやすかったから, 今の説明でわからなかった人は貼ったWikipediaの記事を見てくれ.

余談だが以下のサイトもわかりやすかったので載せておく

ferret-plus.com

C言語UTF-8の文字を出力

ということで, UTF-8で表現される文字を出力するには以下の流れで処理すれば良さそう.

  1. 先頭4バイトを調べてその文字が何バイトで表現されるかを取得する
  2. そのバイト数分データをOSに渡す

はい.簡単(なぜなら難しい処理は全てOSがやってくれるため).

というわけでコードです.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

// マルチバイト文字のバイナリを標準出力
void print_bin_from_char(char *c, int bytes){
    char *bin = calloc(sizeof(char), 8 * bytes);
    int i, j;
    unsigned int num;
    int bin_digit = 0;
    unsigned char *unsigned_c = (unsigned char *)c;
    for (i = bytes - 1; i >= 0; i--)
    {
        num = (unsigned int)unsigned_c[i];
        for (j = 7; j >= 0; j--)
        {
            bin[8 * i + j] = '0' + num % 2;
            num = num / 2;
            bin_digit++;
        }
    }
    i--;
    j--;
    write(1, "0b", 2);
    for (i = 8 * bytes; i > bin_digit; i--) // 0埋め
        write(1, &"0", 1);
    for (i = 0; i < bin_digit; i++)
        write(1, bin + i, 1);
    write(1, "\n", 1);
    free(bin);
}

// UTF-8 で何バイトで表現されるか取得
int get_byte(char *c){
    if (!(*c & (1 << 7))) // 1ビット目が0の時は1バイト
        return (1);
    int bytes = 0;
    for (int i = 7; i >= 4; i--)
    {
        if (*c & (1 << i))
            bytes++;
        else
            return (bytes);
    }
    return (bytes);
}

int main(){
    char *c = "あ"; // 11100011:10000001:10000010
    printf("\n%s bytes: %d\n", c, get_byte(c));
    print_bin_from_char(c, get_byte(c));
    char c_aa[3] = {0b11100011, 0b10000001, 0b10000010};  // テスト用
    write(1, c_aa, 3);
    
    char *c1 = "a";
    printf("\n%s bytes: %d\n", c1, get_byte(c1));
    print_bin_from_char(c1, get_byte(c1));
    char c_a = 0b01100001;
    write(1, &c_a, 1);
    
    char *c2 = "À";
    printf("\n%s bytes: %d\n", c2, get_byte(c2));
    print_bin_from_char(c2, get_byte(c2));
    
    char *c3 = "🤔";
    printf("\n%s bytes: %d\n", c3, get_byte(c3));
    print_bin_from_char(c3, get_byte(c3));
    char thinking[4] = {0b11110000, 0b10011111, 0b10100100, 0b10010100};  // テスト用
    write(1, thinking, 4);

    return (0);
}

実行結果

あ bytes: 3
0b111000111000000110000010
あ
a bytes: 1
0b01100001
a
À bytes: 2
0b1100001110000000

🤔 bytes: 4
0b11110000100111111010010010010100
🤔

という感じで出力できた.

バイト数取得の部分は文字列リテラルからして最後にNULL文字 \0 が入っていることが確定しているのでまぁそれで判定しても良かったのだが, 今回はUTF-8のフォーマットを知りたいというのが目的としてあるのであえて各ビットを調べてバイト数を取得するようにした.

UTF-8の素晴らしい部分としてはASCIIと互換性があることかなぁって感じ. 今回のプログラムの実行結果を見ればわかるんだが, a の値がASCIIと同じなんだなぁ. これはUTF-8Wikipediaのページを見れば書いてあるんだが基本的にASCIIコードというのは 0x00~0x7F までで表現されている. これをバイナリにで見ると 0b00000000 ~ 0b01111111 となり, なんと先頭1bit目が0なので1バイトで表現するというのが表現出来ていて, ASCIIと互換があるという感じ.

いや〜考えた人マジ天才っすな.

UTF-8の素晴らしさを知ったところで今日はこのへんで閉めまーす. ばいばーい👋

👋 bytes: 4
0b11110000100111111001000110001011
👋

ASCIIコードについてはWikipediaを見てくれ

ja.wikipedia.org

プログラム完全版

感想

今回はWikipediaのページを参考にしながらバイナリレベルでUTF-8の文字表現について見てみた.

いや〜マジで賢い仕様だなぁという感想(小並感).

今回はC言語UTF-8で表現されたバイナリを見て実際に仕様通りにバイナリデータを取得でき, それを標準出力に出力するというところまでやってみた.

UTF-8に関する記事を上げるなら, 自作OSでUTF-8対応とかやってみたいなぁなんて思ったり...