Not only is the Internet dead, it's starting to smell really bad.:2021年04月30日分

2021/04/30(Fri)

[ソースコード考古学] glibcとPOSIX:2001とのstrerror_r(3)のプロトタイプの混乱について

kbkさんとこより、 strerror_r(3)ネタ。

このstrerror_r(3)の混乱については、そもそもエラーメッセージって元はV7 UNIXにおいては sys_errlist(3)つって、文字列定数の配列から直接拾ってくるもんだったということを知っておくべき。

例としてエラーメッセージを標準出力に表示するする関数である perror(3)のソースコード、抜粋するまでもなく短いから全文でいいな!おのれSCO!訴えるなら訴えてみろ!

/*
 * Print the error indicated
 * in the cerror cell.
 */

int	errno;
int	sys_nerr;
char	*sys_errlist[];
perror(s)
char *s;
{
	register char *c;
	register n;

	c = "Unknown error";
	if(errno < sys_nerr)
		c = sys_errlist[errno];
	n = strlen(s);
	if(n) {
		write(2, s, n);
		write(2, ": ", 2);
	}
	write(2, c, strlen(c));
	write(2, "\n", 1);
}

エラーが起きた時に更なるエラーが発生することの無いよう、この"Unknown error"とsys_errlist配列にある静的なメッセージ以外を使うべきでは本来なかったのよね。

ところが4.4BSDに導入された strerror(3)において

なんて余計な拡張したのが失敗だったんだよな。

こっちはライセンス文が長いのでコード部だけ。

char *
strerror(num)
	int num;
{
	extern int sys_nerr;
	extern char *sys_errlist[];
#define	UPREFIX	"Unknown error: "
	static char ebuf[40] = UPREFIX;		/* 64-bit number + slop */
	register unsigned int errnum;
	register char *p, *t;
	char tmp[40];

	errnum = num;				/* convert to unsigned */
	if (errnum < sys_nerr)
		return(sys_errlist[errnum]);

	/* Do this by hand, so we don't include stdio(3). */
	t = tmp;
	do {
		*t++ = "0123456789"[errnum % 10];
	} while (errnum /= 10);
	for (p = ebuf + sizeof(UPREFIX) - 1;;) {
		*p++ = *--t;
		if (t <= tmp)
			break;
	}
	return(ebuf);
}

はい、"Unknown error"のケツに更にエラー番号を付け加えとりますね。

んでglibcはスレッドセーフ拡張としてstrerror_r(3)を発明するわけだけど、上記の*BSDのstrerror(3)仕様をそのまま拡張したので(libc5はそもそも*BSDのlibc由来だしな)

GNU 仕様の strerror_r() は、 エラーメッセージを格納した文字列へのポインターを返す。
返り値は、 この関数が buf に格納した文字列へのポインターか、 何らかの (不変な) 静的な文字列へのポインター、 のいずれかとなる (後者の場合は buf は使用されない)。
buf に文字列が格納される場合は、 最大で buflen バイトが格納される (buflen が小さ過ぎたときには文字列は切り詰められ、 errnum は不定である)。
文字列には必ず終端ヌル文字 ('\0') が含まれる。  

という「渡した静的バッファには何も書きこまれないケースの方が多い」という一見奇怪な仕様が生まれたわけ、でもstrerror(3)の実装の中身を知ってしまえば疑問に思うことは無いと思う。

ではなぜPOSIX:2001のstrerror_r(3)はglibc2とは異なるI/Fになってしまったのかというと、それは我々非英語民と ブタが悪いのです。

エラーメッセージだって国際化してほしいという要求により、例えば*BSDでは catgets(3)によるメッセージ文字列のうん国際化が突っ込まれたのよね、邪魔なのでifdefは削除した後のコード

extern char *sys_errlist[];
extern int sys_nerr;

char *
__strerror(num, buf)
	int num;
	char *buf;
{
#define	UPREFIX	"Unknown error: %u"
	register unsigned int errnum;

	nl_catd catd ;
	catd = catopen("libc", 0);

	errnum = num;				/* convert to unsigned */
	if (errnum < sys_nerr) {
		strcpy(buf, catgets(catd, 1, errnum, sys_errlist[errnum])); 
	} else {
		sprintf(buf, catgets(catd, 1, 0xffff, UPREFIX), errnum);
	}

	catclose(catd);

	return buf;
}

よってsys_errlistの文字列定数をそのまま使うことは無くなり、strerror(3)は常に静的なバッファにメッセージをコピーする仕様に変わったのだ。

ちなみに386BSDの頃はまだ4.4BSDのままなので誰がはじめた物語かというと やっぱりNお前か。 このjtc@って人はNのlibcの初期の国際化まわり、つまりsetlocale(3)なんかの実装を入れた人やね。

ちなみに386BSD(バハマドル)は本日の為替レートでJPY 42,027.68でございます。

まぁエラーメッセージを国際化するってのはエラーの上にエラーを重ねる可能性が高いので愚の骨頂なのだけども、時代が1994年ならしやーない。 おそらく当時の商用UNIXも同じような事やっとったからjtc@も作業したんだと思うけど、当時のことは1994年ってついこの間じゃんと思ってそうなお年寄りにでも聞いてください。

ということでPOSIX:2001においては常にエラーメッセージは静的バッファにコピーされることを前提にスレッドセーフ対応がされることになる。 なので

int strerror_r(int errnum, char *strerrbuf, size_t buflen);

というglibcとは異なるプロトタイプで仕様化されたということだ。

そもそもerrno(3)自体がなぁ、スレッドセーフという概念の無い時代の仕様のものを無理矢理スレッドローカル変数です!と誤魔化したり、やることなすこと後手後手でゴテゴテなのである。

はい、 なぜ localtime(3) の引数は time_t のポインタなのか?以来のソースコード考古学のお時間でした。 その時も書いたけど、Cの場合はどんなに理不尽と思える仕様があっても、それには必ず何らかの歴史的理由があるということだ。

それじゃバイバーイ。