2007/01/18(Thu)
○ どのようにしてlibcは後方互換を保つのか?(その1)
国際標準規格(ISO)としてのCは現在のところ
- ISO/IEC 9899:1990 (C90)
制定 - 米国内規格 ANSI X3.159-1989(C89) と同じ - ISO/IEC 9899/AMD1:1995 (C90:AMD1 あるいは C95)
追補 - 国際化対応関数(mbrtowcなど)の追加 - ISO/IEC 9899:1999 (C99)
改訂 - 変更点多すぎなんで このへん読んでください
と3つのバージョンが存在します、これらはすべて後方互換が保障されています。
ソースレベルでの後方互換とは
- C90で書かれたソースコードは一切の修正なしでC99でコンパイル可能
ということに尽きるのですが、ここでひとつ問題が発生します。それは名前の衝突です。
C90:AMD1やC99の改正ではいくつかの新しい標準関数が追加されました。
代表的なものは
$ man 3 mbrtowc
...
STANDARDS
The mbrtowc() function conforms to ISO/IEC 9899/AMD1:1995 (``ISO C90,
Amendment 1''). The restrict qualifier is added at ISO/IEC 9899:1999
(``ISO C99'').
$ man 3 atoll
STANDARDS
The atoll() function conforms to ISO/IEC 9899:1999 (``ISO C99'').
などです。
しかしCにはC++のような名前空間はないですし、標準関数の命名規則やユーザ定義関数名の制限
(例えば"stdc_"ではじまる名前は標準関数に予約しユーザには使用を禁ずるなど)もありません。
つまりmbrtowcやatollという関数名はすでにユーザが別の目的に使っていたかもしれないのです。
以下は簡単なサンプルです。
test.c
--
#include <stdio.h>
#include <stdlib.h>
void
atoll(void)
{
printf("hello, world.\n");
}
int
main(void)
{
atoll();
return 0;
}
atollという関数名はC90の頃は自由に使っていい名前だったはずなのに
FedoraCore6(glibc-2.5 + gcc-4.1.1)ではコンパイルに失敗します。
gcc -o test test.c
test.c:5: error: conflicting types for 'atoll'
/usr/include/stdlib.h:159: error: previous declaration of 'atoll' was here
このままではソース互換とはいえません。
この衝突を防ぐため提供される機能が事前定義マクロ__STDC_VERSION__です。
事前定義マクロというのは、何かヘッダをインクルードせずとも予め定義されるマクロです。
__名前__
のように前後にアンダースコアが2つつきます。
規約にはISO Cのバージョン情報として、以下の事前定義マクロあります。
#define __STDC__ 1 /* ISO-Cである */
#undef __STDC_VERSION__ /* C90では定義されない */
#define __STDC_VERSION__ 199409L /* C90:AMD1の場合 */
#define __STDC_VERSION__ 199901L /* C99の場合 */
この値をセットするのはプリプロセッサです。
gccの場合-stdオプションを使うことで__STDC_VERSION__を制御できます。
- C90の場合
$ echo "__STDC_VERSION__" | gcc -std=c89 -E - # 1 "<stdin>" # 1 "<built-in>" # 1 "<command line>" # 1 "<stdin>" __STDC_VERSION__
- C90:AMD1の場合
$ echo "__STDC_VERSION__" | gcc -std=iso9899:199409 -E - # 1 "<stdin>" # 1 "<built-in>" # 1 "<command line>" # 1 "<stdin>" 199409L
- C99の場合
$ echo "__STDC_VERSION__" | gcc -std=c99 -E - # 1 "<stdin>" # 1 "<built-in>" # 1 "<command line>" # 1 "<stdin>" 199901L
規約どおりですね。
ではこのマクロをどう使ってるのか、実際にglibc-2.5のヘッダファイルを見てみましょう。 まずは features.hです。
#if (defined _ISOC99_SOURCE || defined _ISOC9X_SOURCE \
|| (defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L))
# define __USE_ISOC99 1
#endif
__STDC_VERSION__が199901L以上の場合、__USE_ISOC99が定義されます。
ではさっき名前衝突したatoll(3)の宣言を見てみましょう。
stdlib.hです。
#if defined __USE_ISOC99 || (defined __GLIBC_HAVE_LONG_LONG && defined __USE_MISC)
__BEGIN_NAMESPACE_C99
/* Convert a string to a long long integer. */
__extension__ extern long long int atoll (__const char *__nptr)
__THROW __attribute_pure__ __nonnull ((1)) __wur;
__END_NAMESPACE_C99
#endif
atoll(3)は__USE_ISOC99が定義されている場合のみ有効になっています。
OpenBSDの場合もほぼ同様です、定義されてるヘッダは
sys/types.hになります。
#if defined(_ISOC99_SOURCE) || (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901)
# undef __ISO_C_VISIBLE
# define __ISO_C_VISIBLE 1999
#endif
同様に stdlib.hです。
#if __ISO_C_VISIBLE >= 1999
long long
atoll(const char *);
...
※ NetBSDについてはここでは解説しません、理由はのちほど。
つまりgcc -stdc=c89で名前衝突は避けられるということです。
$ gcc -std=c89 -o test test.c
$ nm test | grep atoll
08048364 T atoll
$ ./test
hello, world.
今度は問題なくコンパイルできました。
この他にも
- X/Open Portability Guide(XPG)、IEEE Std 1003.1(POSIX)で導入された関数
iconv(3)など - 4.4BSDとの互換性の為の関数
isblank(3), strtoq(3)など - GNU拡張で追加された関数
fmemopen(3), open_memstream(3)など - NetBSDがやむを得ず追加した独自の関数
vsnprintf_ss(3) orz - OpenBSDがやむを得ず追加した独自の関数
strtonum(3) orz orz
などの名前空間保護もあるのですが、手法は同じなので解説を割愛させていただきます。
次回はバイナリレベルでの後方互換性の話です。
○ NetBSD
今回なぜFC6使って解説してるかというと...それはNetBSDが(ry
○ めも
B-Treeでなくskip listsアルゴリズムを使ったDBM、
SkipDB。sleepycat BDB4やqdbmより速いらしい。
ライセンスは3-clause BSDL、そのうち試してみよ。