I know I believe in nothing but it is my sweet nothing.:2022年09月21日分

2022/09/21(Wed)

[386BSD(の子孫)ソースコードシリーズ (334)] sys/cdefs.hとは何ですか?番外編 - __UNCONST()マクロとは何か?

以前書きかけてもう長いこと放置してる 連載記事をちょっとだけ復活、というのも128bitポインタ時代に備えよみたいなことを6億円振込を待つ世界緊急放送で受信したので。

要はUN*X方面だとILP32/LP64を前提としてポインタをlongとして扱うクソコードが大量に残っていて

というお話でいいんだっけ、つーかお前ら128bitポインタ時代にまだC言語使う気ですか(戦慄) intwidest_tといいもうCは捨てようぜ。

まぁワイは将来P128/LP128と呼ぶのか知らんがその頃には生きてないからどうでもよろしい、生き延びてもSandyおじさんだろうし…というか現在C2Dおじさんまで退化している(X61でこれ書いてる)。 ところで128bit CPUって命令セットはx86_128なんですかやっぱり。

ちなみにCの仕様だとint64_tは対応するbuilt-in typeが無いなら実装してはならない、なので代わりにint_{fast,least}64_tを使えが正しい。しかし世の中そんなコードはまず存在しないというのは 以前にも書いた通り。そうintN_tを使ってる時点でそれはILP32とLP64の間でしか移植性は無いのだ(無慈悲)。

ところでワイにとってはカーネルって道頓堀に沈んでたおじさんの像でしかないので、sysの下とか全く知らんからそんなに大量にポインタをlongで扱ってるコードが生き残ってるの?という感じではあるな。 クッソ適当に知識多く知恵少なきインターネットを検索すると通信用語の基礎知識の LP64の項に

ポインターをint型に代入するようなプログラムは移植できないが、long型に代入するようなプログラムは
データ長が同じため問題が顕著化せず、そのまま移植することができる。

UNIXでは伝統的にポインターをlong型変数に入れて使う傾向にある。
例えばコールバック関数の引数でunsigned longにポインターが入っており、これを(void *)などで
キャストして使う、といったこともよくある。そのため、このデータ型モデルが採用されたものと思われる。

なんて書かれてるけどさ、今年ってさぁ ISO/IEC西暦2022年だよね…

あっそうだ(唐突に本題)、ワイの知ってる範囲でもconst外し(関節外しみたいなんやなw)マクロであるcdefs.hの__UNCONST()でポインタをunsigned longで扱っとるから移植性ないよねこれ。

/*
 * The following macro is used to remove const cast-away warnings
 * from gcc -Wcast-qual; it should be used with caution because it
 * can hide valid errors; in particular most valid uses are in
 * situations where the API requires it, not to cast away string
 * constants. We don't use *intptr_t on purpose here and we are
 * explicit about unsigned long so that we don't have additional
 * dependencies.
 */
#define __UNCONST(a)    ((void *)(unsigned long)(const void *)(a))

まぁマクロなのでここ一か所だけ修正すりゃ終わりなんだが、逆にマクロ使わずにconst外しをunsigned longでやられてると修正が大変になるパターンではある。

なおこの__UNCONST()マクロ、なぜダイレクトに非const型にキャストせずこんなもんを介すのかというと、gccに-Wcast-qualというオプションつけるとconstから非constへのキャストは警告の対象になってるからなんよね。 そしてNではbsd.sys.mkでWARNS>2がMakefileで指定されてると-Wcast-qualが有効になりかつ-Werrorがデフォルトで有効なのでコンパイルエラーになるのだ。

.if ${WARNS} > 2
CFLAGS+=        -Wcast-qual -Wwrite-strings
CFLAGS+=        -Wextra -Wno-unused-parameter
...
.endif

でもさ、そもそもconstから非constへのキャストは何らかのミスの可能性があるってだけで、仕様としては合法なので-Werror -Wcast-qualして警告をエラーとして扱ってるのが間違いなんじゃね?ではあるんだよね。 ということで脳死でいたずらに警告レベルを上げた結果、いらんマクロを発明してしまったんじゃねえかなぁこれ。

ちなみにOでは__UNCONSTみたいなものは用意せずそのまま非constにキャストして-Wno-cast-qualしてたはず。

とはいえ合法だがconstを外すなと古事記とセキュアコーディングガイドにも 書かれているよねって原則論が無いわけではない、しかしmalloc(3)したメモリをconstポインタに代入したけど、free(3)する時に元の非constポインタが残ってなくてconstポインタで開放したくてconstを外すなんてコードにはよく遭遇するからな。 というかそういうコードが出す警告のために__UNCONST()の99%は使われてるんですわ。

そしてNだとサードパーティー製のソフトウェアをsrc/external以下に大量に抱えてるのだけど、いちいち-Wcast-qual対策で__UNCONST入れてってるのよね。 非常にめんどくさい作業だし、バージョン上げる際にvendor importするとその部分でconflictも発生しかねないわけでさ、なるべく差分は最小にとどめたいのに。 できればオレオレN6ではOみたいに-Wcast-qualをエラー扱いにするの止めたいんというお気持ち。

そんで128bitポインタ問題に話は戻るけど、Fにおける同機能に__DECONST()マクロがあるんだけどこっちは__uintptr_t(C99のintptr_tの内部型)を使ってるのでこちらでは発生しない、ただしいちいち_types.hもインクルードする必要があってより差分が増えるのである。

#ifndef __DECONST
#define __DECONST(type, var)    ((type)(__uintptr_t)(const void *)(var))
#endif

つーか_types.hはcdefs.hをインクルードしてるので本来なら循環参照じゃねえのこれ、ガード入ってるから誰も気づかないだけで。 そもそもcdefs.hはMI/MDを扱わない *1のでこの中でintptr_tを使うのは実装として変なんだよな。

でもこのシリーズ中で以前も説明したけど、cdefs.hの役割というのは

の黒魔術、よってコンパイラの警告対応だからcdefs.hに実装するのはそれはそうであって悩ましいのである。

本来ならコンパイラ側に局所的に警告を抑制するpragmaなりattributeが欲しいとこ、lint(1)なら/*LINTED*/コメントで抑制できるんだけど。 いまだと翻訳単位(ソース単位)でしか警告オプション切り替えられないからな。

ただコードを書き換えるにつれ一度は警告を抑止した部分が突然牙を剥いたりすることもあるので、やっぱりコードそのものを正しく書き直せが正解かな。

*1:ただしa.out/ELFのようなobject formatの差異はここで扱う