2009年2月23日月曜日

C言語におけるUTF-8の取り扱い確認


/*

ちょっと調査。

まずemacsでひらがなの「が」をファイルに書くとLinux
とOSXでは違いがあるのかどうか。そのファイルをodで比
べてみる。

まずOSX。

----
$ od -t x1 -t c HIRAGANA-LETTER-GA.txt
0000000 61 62 63 64 0a e3 81 8c 0a
a b c d \n が ** ** \n
0000011
----

続いてLinux。

----
$ od -t x1 -t c HIRAGANA-LETTER-GA.txt
0000000 61 62 63 64 0a e3 81 8c 0a
a b c d \n 343 201 214 \n
0000011
----

違いは無い。

さて、e3 81 8c をUTF-8にて解釈してみる。

ビットにすると、

----
CL-USER(13): (dolist (n (list #xe3 #x81 #x8c))
(format t "~b " n))
11100011 10000001 10001100
NIL
----

である。このビットパターンは、UTF-8としては

1110yyyy 10yxxxxx 10xxxxxx

であり、最大16bitのコードポイントを表現している。

具体的にコードポイント対応部分を抽出すると、

0011 000001 001100

さらにこれを右詰めoctetになおすと、

00110000 01001100

----
CL-USER(14): (dolist (n (list #b00110000 #b01001100))
(format t "~x " n))
30 4c
NIL
----

というわけで、これはNFCの「が」(U+304C)だ。

とどのつまりファイルの中身にはOSは関知していないか
ら、emacsやodが文字をUTF-8のNFCで取り扱っており、そ
こに一貫性がある、ということだろう。

続いてgccのプリプロセッサまでの処理における文字の取
り扱いを確認したい。

前章で実験したところ、gccはC99に完全準拠していなかっ
た。ソースファイルの識別子に多バイト文字が使われて
いた場合は、それをプリプロセス前に国際文字名
(\uxxxx)に変換しなければいけないのだが、それをやっ
てくれていないようだ。それを確認する。

*/
----
#include <stdio.h>
int main(void)
{
int が;
for (が=1;が<10;が++)
printf("%d ", が);

return (0);
}
----
/*

こんなソースでためす。これのod。

----
$ od -t x1 -t c i18n-character-name-2.c
0000000 23 69 6e 63 6c 75 64 65 20 3c 73 74 64 69 6f 2e
# i n c l u d e < s t d i o .
0000020 68 3e 0a 69 6e 74 20 6d 61 69 6e 28 76 6f 69 64
h > \n i n t m a i n ( v o i d
0000040 29 0a 7b 0a 20 20 20 20 20 69 6e 74 20 e3 81 8c
) \n { \n i n t が ** **
0000060 3b 0a 20 20 20 20 20 66 6f 72 20 28 e3 81 8c 3d
; \n f o r ( が ** ** =
0000100 31 3b e3 81 8c 3c 31 30 3b e3 81 8c 2b 2b 29 20
1 ; が ** ** < 1 0 ; が ** ** + + )
0000120 0a 20 20 20 20 20 20 20 20 20 20 70 72 69 6e 74
\n p r i n t
0000140 66 28 22 25 64 20 22 2c 20 e3 81 8c 29 3b 0a 0a
f ( " % d " , が ** ** ) ; \n \n
0000160 20 20 20 20 20 72 65 74 75 72 6e 20 28 30 29 3b
r e t u r n ( 0 ) ;
0000200 0a 7d 0a
\n } \n
0000203
$
----

これをgcc -Eで処理。

includeしているから長くなるが、とにかく変換はしていないようだ。

*/
----
# 1 "i18n-character-name-2.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "i18n-character-name-2.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 64 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/_types.h" 1 3 4
# 27 "/usr/include/_types.h" 3 4
# 1 "/usr/include/sys/_types.h" 1 3 4
# 32 "/usr/include/sys/_types.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 33 "/usr/include/sys/_types.h" 2 3 4
# 1 "/usr/include/machine/_types.h" 1 3 4
# 34 "/usr/include/machine/_types.h" 3 4
# 1 "/usr/include/i386/_types.h" 1 3 4
# 37 "/usr/include/i386/_types.h" 3 4
typedef signed char __int8_t;


... 中略 ...


static __inline int __sputc(int _c, FILE *_p) {
if (--_p->_w >= 0 || (_p->_w >= _p->_lbfsize && (char)_c != '\n'))
return (*_p->_p++ = _c);
else
return (__swbuf(_c, _p));
}
# 2 "i18n-character-name-2.c" 2
int main(void)
{
int が;
for (が=1;が<10;が++)
printf("%d ", が);

return (0);
}
---
/*

では、文字列定数の中だとちゃんとやってくれるのか?
というところで同じソースを変更して確認する

*/
----
#include <stdio.h>
int main(void)
{
int i;
for (i=1;i<10;i++)
printf("%d が", i);

return (0);
}
----
*/
/*

あり、だめだ。次のように、変換していない。

*/
----
前略 ...

static __inline int __sputc(int _c, FILE *_p) {
if (--_p->_w >= 0 || (_p->_w >= _p->_lbfsize && (char)_c != '\n'))
return (*_p->_p++ = _c);
else
return (__swbuf(_c, _p));
}
# 2 "i18n-character-name-3.c" 2
int main(void)
{
int i;
for (i=1;i<10;i++)
printf("%d が", i);

return (0);
}
----
/*

というわけで、gccの字句関係処理はC99の要求まんまに
実装されているのではなさそうだ。ソース文字集合が
UTF-8そのものである、という作りに見受けられる。

さて、次にファイル名の取り扱いを確認したい。ファイ
ルの中身とちがって、ここはOSの関与がある部分だ。
"が.txt"というファイルを作成する。これの中身を表示
するプログラムで振舞いを確認してみる。APIはCの標準
ライブラリを使う。これは、予測としては不一致なくう
まくいくはず。gccの標準ライブラリがfopenの引数たる
ファイル名をOSXならOSX向けに取り扱ってくれるという
予測だ。

----が.txt---
abcd

-------------

*/
----ga-opne.c----
#include <stdio.h>

int main(void)
{
FILE *fp;
fp = fopen("が.txt", "r");

if (fp == NULL)
printf("failed.\n");
else {
printf("Succeed.\n");
fclose(fp);
}
return (0);
}
--------
/*

----
$ gcc -std=c99 ga-open.c
$ ./a.out
Succeed.
$
----

うまくいった。

さて、ファイル名の取得はどうだろう。ディレクトリに
含まれるファイルの名前を取得する方法は標準Cにはない。
よって、ここから先は処理系次第となる。ただし処理系/環境
がPOSIXだとかSUSとかに対応しているなら、それらAPIを
使うということである程度のポータビリティは期待でき
る。

さて、この領域は、Advanced Programming in the UNIX
Environment (apue.2e)だろう、ということでapue.2eか
らサンプルをとってくる。

----ls1.c----
*/
#include "apue.h"
#include <dirent.h>

int
main(int argc, char *argv[])
{
DIR *dp;
struct dirent *dirp;

if (argc != 2)
err_quit("usage: ls directory_name");

if ((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
while ((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);

closedir(dp);
exit(0);
}
/*
--------

apue.2eのサイトにあがっているソースをLeopard上でコ
ンパイルするには多少調整が必要。apue.2eが発刊された
ときは、10.3.xだったのだ。それから10.4、10.5とかわ
るなかでOSXはPOSIX対応を進めたため、ヘッダ関係で混
乱があるようだ。

いくつか調整作業をしてmakeと次のコマンドでls1.cをコ
ンパイルした。

gcc -ansi -I/Users/aka/scratch/c-ref/apue.2e/include -Wall -g -DMACOS -L../lib ls1.c ../lib/libapue.a -o ls1

で、そのls1で、が.txtを含むga-testディレクトリの中
身をOSから取得する。



(gdb) n
..
(gdb) p dirp->d_name
$10 = "..\000\000?\r\000\024\000\b\nが.txt\000*", '\0' <repeats 231 times>
(gdb) n
(gdb) p dirp->d_name
$11 = "が.txt\000*", '\0' <repeats 243 times>
(gdb) p/x dirp->d_name
$12 = {0xe3, 0x81, 0x8b, 0xe3, 0x82, 0x99, 0x2e, 0x74, 0x78, 0x74, 0x0, 0x2a, 0x0 <repeats 244 times>}
(gdb)

で、ここでメモリに入ってる

0xe3, 0x81, 0x8b, 0xe3, 0x82, 0x99

が、"が"なのだが、これを先程と同じ方法でUTF-8として
読み解くと、

U+304b U+3099
[HIRAGANA LETTER KA] [COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK]

であることがわかる。ここでNFDがあらわれた!

そのあとは、printfで%sしてるだけなので、この
UTF-8(NFD)が正しく表示されるかは、その表示を担当す
るものによりけりになる。上記のls1->gdb->emacs->screen->Terminal
では実は正常に表示されず、が.txtの"が"と"."の間に妙な文字が表示
されてしまう。(コピペした際に上のgdb出力のこの異常は消えちゃった)
Terminal直ならば問題なくて、

------------
$ ./ls1 ga-test
.
..
が.txt
$
------------

となる。

さて、ここまで来たところで、apue.2eのreaddirの出自
を確認する必要がある。

apue.2eによると、POSIX.1とのこと。
とするとmanにあるのか? man readdir、あった。

-----------
DIRECTORY(3) BSD Library Functions Manual DIRECTORY(3)

NAME
closedir, dirfd, opendir, readdir, readdir_r, rewinddir, seekdir, telldir --
directory operations

LIBRARY
Standard C Library (libc, -lc)

SYNOPSIS
#include <dirent.h>

...
-----------

するとdirentもmanにあるのか? あった。

-----------
DIR(5) BSD File Formats Manual DIR(5)

NAME
dir, dirent -- directory file format

SYNOPSIS
#include <sys/types.h>
#include <sys/dir.h>

DESCRIPTION
Directories provide a convenient hierarchical method of grouping files while
obscuring the underlying details of the storage medium. A directory file is
differentiated from a plain file by a flag in its inode(5) entry. It consists
of records (directory entries) each of which contains information about a file
...
-----------

で、

-----------
*/
struct dirent {
ino_t d_ino; /* file number of entry */
u_int64_t d_seekoff; /* length of this record */
u_int16_t d_reclen; /* length of this record */
u_int16_t d_namlen; /* length of string in d_name */
u_int8_t d_type; /* file type, see below */
char d_name[MAXPATHLEN]; /* name must be no longer than this */
};
/*
-----------

なそうであり、d_nameの中身がどうあるべきかは無い。
まあ、ファイルシステムは環境によって違うからなぁ。

というわけで、このPOSIX.1のreaddir経由でOXとアプリ
がファイル名をやりとりする場合は、エンコードについ
てはアプリ側がOSが何を選択しているかをどこかで知っ
た上で使わないといけないということだ。
*/

これで、Subversionにおけるファイル名の取り扱いを探る準備ができたのかなぁ。

こつこつ。

0 件のコメント: