The Man Who Fell From The Wrong Side Of The Sky:2007年1月18日分

2007/1/18(Thu)

どのようにしてlibcは後方互換を保つのか?(その1)

国際標準規格(ISO)としてのCは現在のところ

と3つのバージョンが存在します、これらはすべて後方互換が保障されています。

ソースレベルでの後方互換とは

ということに尽きるのですが、ここでひとつ問題が発生します。それは名前の衝突です。

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__を制御できます。

規約どおりですね。

ではこのマクロをどう使ってるのか、実際に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.

今度は問題なくコンパイルできました。

この他にも

などの名前空間保護もあるのですが、手法は同じなので解説を割愛させていただきます。

次回はバイナリレベルでの後方互換性の話です。

NetBSD

今回なぜFC6使って解説してるかというと...それはNetBSDが(ry

めも

B-Treeでなくskip listsアルゴリズムを使ったDBM、 SkipDB。sleepycat BDB4やqdbmより速いらしい。
ライセンスは3-clause BSDL、そのうち試してみよ。