Tentative Definition とは何か

過去回 で settimeofday(2) の解説に以下のコード

16 struct	timeval time;

を引用した時に思ったんだけど、ヘッダファイルに書かれてるという先入観で

extern struct timeval time;

と勘違いして、本体はどこで宣言されてるんだろうとソースを探し、どこにも見つからなくて途方に暮れる C 初心者っているよね(というか俺です)。

しかし extern 無しなんだから kernel.h をインクルードした翻訳単位(要は *.c ファイル)はすべて time を宣言してると同義ってことに気づけると C 中級者くらいにはなれたはずである(ヤッター)。

ええ…これリンク時に multiple definition of `time’ で重複エラーになるんじゃないの?と疑問に思ったそこの若者(壁と会話する患者)、実は C の仕様上 100% 完全に(死んだオウムくらい)合法なのだ。 これは Tentative Definition といって、日本語では仮定義あるいは暫定定義と訳される仕様なのだ。

JIS X 3010:2003 「6.9.2 外部オブジェクト定義」から引用すると

ファイル有効範囲のオブジェクトの識別子を, 初期化子を使わず, かつ, 記憶域クラス指定子なしか 又は記憶域クラス指定子 static で宣言する場合, そのオブジェクトの識別子の宣言を 仮定義 (tentative definition) という。

例 1. から抜粋

int i1 = 1;	// 定義, 外部結合

int i1;		// 正しい仮定義, 前の定義を参照する

正しい仮定義は以前の定義を参照するとコメントにあるけど、実際にどう処理されるか判りづらいよね。 正解はリンカがオブジェクトをリンクする際に全部ひとつの記憶域としてまとめてくれる、である。 これで安全安心一件落着。

古いコードだとこの仕様に依存しまくっているので無駄に検索かけて途方にくれないようにね!

古い時代のルーズな書き方ではあるので gcc では昔からこの外部結合の仮定義を禁ずる -fno-common スイッチが存在した、でもまぁみんな存在自体知らないよね。

ところが gcc10 からデフォルト -fno-common に変更になった

余計なことしやがって(怒)、この結果明示的に -fcommon を指定しないとエラーになってしまうようになった。 -Wcast-qual もだけど合法なのにわざわざ禁止するのはろくな結果を産まないって印象なんだよな。

でもまぁオレオレ N6 も将来的な gcc10 への移行を見越して去年 あらかた全部潰した のだ、肝心の移行作業には手つかずだけどな!

その時気づいたんだが、*BSD はどれもこれも dumprestore.h の修正でこの Tentative Definition の扱いをミスってて 結果として後方互換無くなってね?

これはオレオレ N6 の Issue #342 としてケース切って詳細は書いたのだが(チラシの裏閉鎖してたしな)、かいつまんで説明しようか。

事の起こりは 1979 年

原因を辿っていくと V7 UNIX で埋められた地雷なのでもはや C の原罪といっていいのでは?

13 struct	spcl
14 {
...
26 } spcl;

ファイル名一文字節約するのに dumprestor.h ってのも creat(2) ばりの原罪だよね…

このヘッダは dump(8)restore(8) で使われているが、spcl 構造体のみならず外部定義 spcl も宣言してしまっているのにお気づきだろうか。 まぁ当時としては宣言する手間が省けるわガハハくらいの安易な考えである。

当然このヘッダファイルは *BSD にも受け継がれている。 おお dumprestore.h に名前が修正されてる!

62 union u_spcl {
...
85 } u_spcl;

構造体から共用体になってるのに大した意味はない(おそらく sizeof を変えないためのもの)。 u_ プリフィクスがついたけど外部定義が宣言されてる「バグ」もそのまま引き継がれている。

そして現在

そこからほぼ半世紀が経ったわけだが、gcc10 での -fno-common デフォルト化によりこの「バグ」が顕在化した。 よって 4.4BSD の子孫たちはこのコードを今更ながら修正することになる。

おお F のコミットはカーク・魔球ジック先生じゃないか、やっちまったなぁ!

差分は extern つけるだけの誰がやっても同じ修正だから代表して F でええな。

diff --git a/include/protocols/dumprestore.h b/include/protocols/dumprestore.h
index 65df37af6996..6d22763e96de 100644
--- a/include/protocols/dumprestore.h
+++ b/include/protocols/dumprestore.h
@@ -76,7 +76,7 @@
  */
 typedef uint32_t dump_ino_t;
 
-union u_spcl {
+extern union u_spcl {
 	char dummy[TP_BSIZE];
 	struct	s_spcl {
 		int32_t	c_type;		    /* record type (see below) */

この変更により dump(8) と restore(8) はビルドすると u_spcl が未定義シンボルとなりコンパイルに失敗するようになる。

なので明示的に宣言するように修正を入れている、差分大きいので該当部分だけ。

58 union	u_spcl u_spcl;		/* mapping of variables in a control block */
89 union u_spcl	u_spcl;		/* mapping of variables in a control block */

ん?つまりこれってソース後方互換を失ったってことだよね?

はい。

dumprestore.h があくまで dump(8) と restore(8) 専用のプライベートなヘッダファイルであれば問題ないです。 しかしパブリックであり <protocols/dumprestore.h> に置かれプロトコールとまで言い切ってるんだから

  • 宣言を自分で追加する … ソースコードの書換が必要となる変更だからソース後方互換の喪失
  • 宣言を追加しない … 未定義参照でコンパイルに失敗する変更だからバイナリ後方互換の喪失

と二重の意味で詰んでいる変更なんですわこれ。

でもこんなんどうやって後方互換性保てばいいのよ

原則的には C の仕様上 Tentative Definition は完全に合法なのだから、元に戻して -fcommon つけろが正解である。 局所的に __attribute__((__common__)) つけてもいいし。

しかし extern つけて -fno-common を有効にして今更もう戻せないという場合でも、今から入れる保険あるんです。 えーあるんですかー?

ということでオレオレ N6 ではバランスをとってその案を採用することにした。

どんな方法かというと u_spcl の記憶域を持ったライブラリを用意し、dumprestore.h 使うアプリケーションはそれをリンクすることとするのだ。 なおオレオレ N6 では libdumprestore.a として提供することにした、この コミット を参照。

そもそもヘッダが記憶域なり関数なりを extern するならライブラリがあって当然なのである。

リンカオプションに -ldumprestore を追加する必要はあるけど(ソープ先生なら libc に入れろって言いそう)、ソースは一切変更する必要はない。 ソースが変更でき自分で定義済ならライブラリの存在は無視すればいい。 バイナリ後方互換についても -fcommon ありでも -fno-common でも矛盾は発生せず undefined reference to `u_spcl’ を回避できる。

やったぜ。

ちょっとパラノイア過ぎない?

派手にパラノイアや。