msvc を MicroSoft Visual C++ の略という意味で使っています。
はじめに
C言語では初期値を持たないグローバル変数の宣言は tentative definition,日本語だと仮定義または暫定定義と呼ばれ,C言語特有の特殊仕様になります123。
- インクルードファイルを含む同一ソースファイル内に初期値を持つ定義が他に存在すれば変数の名前と型をコンパイラに伝える「宣言」として扱われますが,存在しなければ初期値ゼロの「実定義」として扱われます。
- 別のファイルに同じ宣言が存在する場合,同一の定義への外部参照宣言として扱われます。
- C++ では必ず定義として扱われるので,重複する場合はエラーになります。C言語との非互換性の一つです。
このように tentative definition は本来C言語の仕様通りで問題ないはずなのですが,バグの温床になりかねないこともあって,昨今のCコンパイラの中にはデフォルトで禁止にするものも増えてきました4。このため自分の書いたコードが他の人の環境ではエラーになることが増えてきたのです。ということで,自分の開発環境下で tentative definition の事前チェックを行えるようにしたい,というのが今回の記事のテーマです。
前提条件
- ソースコードはC言語ですが,一部というか大半が自社製ツールによる自動生成コードであり,これが C90 以前のかなり旧式の仕様です。
- ソースコードには大量の日本語コメントが含まれ,文字コードは Shift_JIS です。ただし,コード本体はマルチバイト文字列を使っていません。
- 筆者の開発環境は Windows11 で,主に Microsoft Visual Studio を使っていますが,LLVM clang も使い始めています。
さまざまな解決案と課題
- C言語のソースコードを C++ としてコンパイルする。
C++ は言語仕様として tentative definition が禁止されているので間違いなく検出できます。ただし,対象のソースコードが古いC言語仕様を使っているので,山のようなエラーが発生して目標を達成する前に死にます。 -
LLVM clang を使う。
clang は tentative definition を検出できます。デフォルトではエラー扱いですが,コンパイルオプションにより警告を揉み消すことも可能です。問題は,たとえコメント内のみであっても Shit_JIS コードは通さないので,全ソースコードを utf-8 に一括変換してからコンパイルする必要があることです。それ以外にもインクルードファイルの大文字・小文字問題も発生しますが,それはコンパイルオプションで揉み消せることが分かっています。 -
GCC を使う。
おそらく Shit_JIS コードを通し,かつ tentative definition を検出できる唯一のメジャーなCコンパイラだと思います。ただ,Cygwin,MinGW/64,MSYS/MSYS2 上で動作させるか,あるいはマイクロソフト謹製の WSL/WSL2 上で使うか,いずれにせよコンパイルチェックのためだけにインストールするには大仰過ぎる感じがして躊躇しています。 - 静的解析ツールを使う。
フリーで使えるメジャーなものといえば cppcheck ですが,残念ながら入力ファイルの文字コードは UTF-8 (BOM) のようです。であれば手間が clang と変わりません。
第5の選択肢~msvc のプリプロセッサを使う
普通の人ならここで諦めてソースコードを utf-8 に一括変換して clang-cl を使うでしょう。しかし,筆者はどうしても中間ファイルを作りたくなかったので,次のように文字コードを変換してパイプで clang-cl に渡すことを考えました。しかし,下記の例だと sample.c の中でインクルードしているヘッダファイルの中で shift_jis を使っていたらダメなのです。また,clang-cl はインクルードファイルの大文字・小文字も気にするので,場合によっては -Wno-nonportable-include-path オプションを追加する必要があります。
iconv -f sjis -t utf-8 sample.c | clang-cl /c /TC /Fosample.obj -
そこで閃きました。あくまで日本語を使っているのはコメント内だけなので,msvc のプリプロセッサを使ってコメントを削除し,その結果を clang-cl に渡せば良いのではないでしょうか。なお,インクルードファイルのパスを指定するオプション /I を追加する場合は msvc 側に付ける必要があります。
cl /E sample.c | clang-cl /c /TC /Fosample.obj -
こうすることで tentative definition を検出できるようになります。
注意事項
ファイル名やディレクトリ名に日本語を使っていると,プリプロセッサ出力に shift_jis コードが含まれてしまいます。
実行例
実行サンプル
サンプルを以下に示します。グローバル変数 v,w,x が衝突します。main.c は Shift_JIS で保存しています。「表」の字は2バイト目が 0x5C (バックスラッシュ)になる有名なダメ文字です。
Visual C の場合
警告レベルを最大 /W4 にしても特に警告もエラーも出ません。
VIsual C++ の場合
C++ モードにすると重複エラーを検出できます。
LLVM clang-cl の場合
重複エラーを検出できましたが,予想通り日本語コメントで警告が出ました。
本記事で編み出した方式の場合
重複エラーを検出できますし,コンパイル時に警告も出ません。
msvc のリンカを使っても大丈夫です。というか,おそらく LLVM のリンカは内部で msvc のリンカを呼び出しています。
この方法を使うと Makefile を全面的に書き直さなくてはならないのが大変です・・・