C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

symlink と Property Sheet で Visual Studio の C++ プロジェクトの構成を整理するメモ

Windows向けアプリの比較的古いプロジェクトを扱う事になると「構成」がカオスで整理したくなる事がしばしばあります。または、これから新しく作るプロジェクトについて、構成が複雑になりそうな場合の参考にもなるかもしれないので、その整理方法についてメモを残しておきます。

このメモでは、

  1. 外部依存ライブラリーのインクルードパスをどのように整理するか? --> symlink がおすすめです
  2. プリプロセッサーの定義をどのように整理するか? --> Visual Studio の Property Sheet の User Macro がおすすめです

について簡単に解説します。

前提

  • Windows 10 でもいまでは symlink を使えます( junction とは別に symlink が使えるようになっています)
    • NTFS 上で自然に扱え、 Git for Windows / Git Bash / cmd / PowerShell などで確認や操作できます
    • 少しだけ注意点があります
  • Visual StudioC++ 開発プロジェクトでも Property Sheet の User Macros を使えます
    • User Macros を使うと msbuild 経由で cl.exe へ任意の Key-Value の組による変数を投げつけられます
    • 少しだけ注意点があります

このメモの対象は、管理する構成、外部依存ライブラリー、ビルド時のプリプロセッサー定義が多いプロジェクト向けです。逆に、例えば、x64向けのDEBUG/RELEASEの2パターンしかなく、外部依存ライブラリーもせいぜい1つか2つ、ソースコードレベルのビルドパターンを分けるための #ifdef 用のフラグも存在しないか、せいぜい1つか2つしかない、そのようなプロジェクトではかえって煩雑になるだけの可能性もあるので、必要を見極めた上で、方法の採否を決めてください。

整理メモ

1. 外部依存ライブラリーのインクルードパスをどのように整理するか?

プロジェクトの構成がカオスに陥っている場合、よくある現象の1つに Additional Include Directories (追加のインクルードディレクトリー)が次のように混乱している事がしばしばあります:

..\..\common\libAAA;C:\Python33\include;library\my_old_mystic_lib;libarry\my_new_lib\include;library2/boost_1_71_0;library2\lpng1637;%(AdditionalIncludeDirectories)

セパレーター文字;で分解すると、

  • ..\..\common\libAAA ← だぶん複数プロジェクトで使いまわしているライブラリー置き場にあるライブラリー
  • C:\Python33\includeWindowsでは"おれおれポジション"な場所へ標準でインストールされるはずのライブラリー
  • library\my_old_mystic_lib ← プロジェクト内で初期に整理された秘伝のおれおれライブラリー
  • libarry\my_new_lib\include ← プロジェクト中期におれおれ的には新設計で追加されたと思われる新型のおれおれライブラリー
  • library2/boost_1_71_0 ← プロジェクトを最近になってテコ入れしようと思って追加したと思われるライブラリー
  • library2\lpng1637 ← プロジェクトをテコ入れしたらユーザーニーズが発生して新たに必要になって追加したと思われるライブラリー
  • %(AdditionalIncludeDirectories)Visual Studio がいつの頃からかつけるようになったよくわからないから触れずにつけてあるおまじない(*1)

と、いった具合です。さらに、プロジェクトの構成がデバッグ、リリースだけではなく、デバッグ・アレ特殊版、リリース・ソレ特殊版、実験用1、実験用2…など増えていて、それにさらにx86、x64があり…。MSVC++のプロジェクトではしばしば、ありがちです。

このメモでは symlink で↑を整理し保守性を改善する方法の提案についてメモを残します。

(*1): %(AdditionalIncludeDirectories) の正体はもちろん"オマジナイ"ではなく意味のある値で、次の (2) で間接的には触れますが、それにフォーカスした詳細の解説はこのメモの趣旨ではないので行いません。

symlink による Additional Include Directories の整理

整理方法:

  1. プロジェクトのルートに include ディレクトリーを作ります
  2. プロジェクトのルートに external_libraries ディレクトリーを作ります
  3. プロジェクトに固有のライブラリー群を external_libraries へ放り込みます
    • 原則的に、できるだけダウンロード等で入手し展開した「そのまま」のディレクトリーを配置するようにします
      • 例えば、 boost や libpng はバージョン番号の付いたディレクトリーを include/boost_1_71_0include/lpng1637 のように配置します
      • できるだけ、この時にいちいちディレクトリーの名前を変更しないように注意します(*1)
  4. include から mklink (cmdなど) または ln (Git Bashなど) 等を使い、 external_libraries (または ..\common\など)へ相対パスsymlink を作成します
    • external_libraries内のライブラリーのディレクトリー構成に include またはヘッダーファイル群が入っているディレクトリーが…
      • 含まれる場合は、その1つ上のパスを(*2)
      • 含まれない場合(直下にヘッダーもソースもごちゃごちゃ置いてある場合など)はそのディレクトリーを(*3)
    • ライブラリーの名称または標準的なディレクトリー名で作成します
      • できるだけ、ファイル単位で必要な .h だけ整理して symlink しようとは考えないようにします(*4)
  5. ライブラリーの管理方法はこのようにする事が開発に関わる人がわかりやすいように README.md などに記しておきます

こうすると、理想的には、 Visual StudioC++ プロジェクトの Additional Include Directory は原則的に全ての構成で共通して

  • .\include;%(AdditionalIncludeDirectories)

だけで済みます(*5)。ライブラリーのバージョンアップのたびにすべての構成の Additional Include Directory の設定値を変更する必要が無くなり、作業コストも減り、構成ごとの設定値を同期し忘れる事故も安全に防げます。

注意点として、この方法に統一したい場合に、 libpng のようにディレクトリーのワンクッションが無くソースコードからの #include <png.h> のように使っていた、あるいは使いたいと考えていた場合、ソースコード#include <png/png.h> のように変更する必要が生じます。これを避けたい場合、ファイル1つだけで済む程度ならばファイル単位の symlink としても事実上の作業コストは変わりませんが、もし複数のファイルをそのように扱う必要がある場合は諦めてワンクッション入れる事をおすすめします。(*4)

なお、外部依存ライブラリーそのものの管理方法も Git Submodule で体系化したりできますが、それは構成の話とは別になるのでこのメモでは触れません。

(*1): ぱっと見の精神衛生を気にすれば boost-1.71.0libpng-1.6.37 あるいは他の統一された命名規則を用意して揃えたくなるかもしれませんが、このメモではおすすめしません。もしそのようなルールを採用する場合は、保守コストが増大したり、複数人の開発者が関わる場合に誤った命名に注意を払う必要が生じたり、そのような面倒の方が大きい事を覚悟した上で導入します。また、もし、同じライブラリーでも複数のバージョンを併存させてビルド構成ごとに分けたい場合などは、 external_libraries/boost/boost_1_71_0 のようにワンクッション入れて整理したくなるかもしれませんが、多くの場合、1つのライブラリーにそれほどたくさんのバージョンを併存させる必要はありませんし、バージョンが併存して扱われている可能性も視認し難くなり、保守コストも増加するので同様にこのメモではおすすめしません。

(*2): 例:

  • include/GLFW -> ../external_libraries/glfw-3.3.bin.WIN64/include GLFW
  • include/xl -> ../external_libraries/libxl-3.8.8.2/include_cpp LibXL

(*3): 例:

  • include/boost -> ../external_libraries/boost_1_71_0 Boost
  • include/png -> ../external_libraries/lpng1637 libpng

(*4): 例えば ligpng のように配布物を展開するとソースとヘッダーが整理されずに配置されている場合などありますが、バージョンの切り替えの際の手間が増え面倒となり、さらにスクリプトを作り半自動化したくなるかもしれません。コスト増加に合理的な理由がない場合はディレクトリー単位で簡単な symlink を用意するだけに留める事をこのメモではおすすめします。どうしても、特定のライブラリーについて直接展開先のディレクトリー直下のファイルを #include <something.h> のように扱いたい場合は、次の (2) でメモを残す Property Sheet に実際のパスを定義する Key-Value を定義した上で、 Additional Include Directories ではその変数を参照させる事で、ライブラリーのバージョンアップのたびに複数の構成の設定値を書き換える手間がかからないようにはできます。

おまけ: Windows や Git Bash で symlink を使える状態に設定にする方法
  1. Windows10を開発者モードにする(設定→更新とセキュリティー→開発者向け(画面左側)→開発者モード)
  2. cmd で setx MSYS winsymlinks:nativestrict
  3. Git Bashgit config --global core.symlinks true

2. プリプロセッサーの定義をどのように整理するか?

構成がカオス化したプロジェクトでは Preprocessor の定義もカオス化していることがしばしばあります。複数のリリースパターンが存在するプロジェクトで #ifdefソースコードレベルでビルドを分岐している場合によく起こります。例えば、

FLAG_X, FLAG_Y, FLAG_P, FLAG_Q, FLAG_R, ... などたくさんのフラグが…

  • X + P ← 初期からある組み合わせ
  • Y + Q ← 初期からある組み合わせ
  • X + P + Q ← ある時点で追加した組み合わせ
  • Y + P + Q + R ← 最近追加した組み合わせ

のようにあり、これらを /D オプション( = Preprocessor の設定値 )で渡してフラグを切り替えた別バージョンとしてビルド、リリースしていた場合、

  • FLAG_S を追加する事にし、これは X + P + Q + S の組み合わせでリリースする事にした…
  • FLAG_Q を廃止する事にした…

そんなような事が起きている事があります。あるいは、デバッグビルドでは特別なフラグを設定して運用している、とか。

何れにせよフラグの管理が合理化されておらず、すべての構成ですべてのフラグを直接管理していると、特にフラグが多ければ多いほど構成の追加や変更に伴いフラグ管理のミスによる誤ったビルド、リリースによる事故が起きたり、そうでなくともたいていセットで使うフラグをプロジェクトの新人がうっかり知らずに片方だけONにしてトラブルになったり…そのような問題に繋がりやすくなります。

Visual Studio の構成の Preprocessor 項のように文字列を与えられる部分ではUser Macros環境変数を使い設定値を整理できます。 (1)で symlink を使ったので、"その手"の手段としては環境変数も使える手段ですが、 WindowsVisual Studiomsbuild を使う場合にはあまり便利ではありません。このメモでは User Macros を使い整理する手法を残します。

User Macros ( Property Manager, Property Sheet, .props ) の仕組みと使い方

  1. View (表示)から Property Manager を表示します
  2. Property ManagerAdd New Project Property Sheet ボタンがあるので、ぽちって MyFlag.props など適当な名前のプロパティーシートを追加します(この名前は簡単に後でも変えられます)
  3. MyFlag のプロパティーVisual Studio で開きます(*1)
  4. User Macros を開き、プロジェクトの構成で整理したい文字列の設定値を作ります(*2)
  5. プロジェクトのプロパティーを開き、(4)で整理(定義)した文字列値を使いたい設定値の部分で $(MY_DEBUG) の形式で使います(*3)
  6. 整理されたフラグはプロパティーマネージャーを使う事を開発に関わる人がわかりやすいように README.md などに記しておきます

こうすると、たくさんの /D オプション用のフラグ管理と構成が必要な場合に、セットで使うはずのフラグの設定漏れをある程度防いだり、構成を追加する最に構成の設定値上で記述するフラグの数を少なくして手間を省いたりできます。

この管理方法を使うと、フラグの削除が必要になった場合の手間はもしかしたら僅かですが増えるかもしれません。ファイル1つからフラグを置換(削除)するか、ファイル2つからするか、というだけの違いなので、何れにしても Visual StudioVSCode の強力で便利でプレビュー付きで安全な正規表現でまとめて処理するのでさほど…とは思いますが、一応書いておきます。

(*1): プロジェクトのプロパティーの画面とほぼ同じなので混乱しそうですが、プロパティーマネージャーの構成とプロジェクトの構成は別に管理されているものなので慌てないでください

(*2): 単純なKey-Valueの文字列変数です。例えば、 MY_DEBUG=_DEBUG;MY_EXTRA_DEBUG_FLAG とか、 FLAG_PQ=FLAG_P;FLAG_Q とか。

(*3): msbuild 向けの "変数的なそれ" の展開方法は3種類あります:

  1. Property element へのアクセス = $(Key) <-- 今回 (2) で Property Sheet の User Macros に追加した Key-Value で使いました
  2. Item element へのアクセス = @(Key)
  3. Item Metadata へのアクセス = %(Key) <-- Visual Studio の作るプロジェクトの構成の Additional Include Directories に標準で追加されている %(AdditionalIncludeDirectories) はこのパターンです (*4)

(*4): プロジェクトの構成にみられる `%(AdditionalIncludeDirectories) はプロジェクトの構成( = .vcxproj)内で循環参照しているわけではなく、別ファイル管理のプロパティーシート( = .props)群の Additional Include Directories 群をバッチ的に展開しています。

参考