見切り発車

とりあえずかきとめたい

C++ をclang で解析するときに情報をvcxproj から取得する方法

この記事はC++ Advent Calendar 2023 の5 日目です。 前に書いたものを簡単に流用しやすくなるように意識してまとめなおしたものです。


C++ でリフレクションやシリアライズを行うためにコードを解析したくなる場合があると思います。

その時の選択肢としてclang(libclang) やそのバインディングを利用するというのは以前から見かける方法です。

さらにWindows でのプログラミングの場合はlibclang に渡すためのインクルードパスなどの情報をVisual Studio のvcxproj から取得しようというアイディアも以前から存在しています。

例えば2017 年のヘキサドライブさんの記事(https://hexadrive.jp/hexablog/program/18139/) や2019 年のランカースさんの記事(http://www.lancarse.co.jp/blog/?p=2885) などです。

上記の記事ではvcxproj ファイルを独自に簡易的に解析して情報を抽出していました。目的によってはそれで十分ですが、Debug ビルドとRelease ビルドで設定が違う、といった場合など少し複雑な条件に対応しようとすると自前での解析はなかなか大変です。

Visual Studio でvcxproj を実際に解析して処理しているのは内部で使用されているMSBuild です。MSBuild の機能はMSBuild API としてC# から利用できるパッケージがNuGet で提供されています。vcxproj の処理はMSBuild API に任せてしまえば後はlibclang でパースしたコードの解析に専念することができます。

以下、C# やNuGet パッケージ、MSBuild の話になりますがそれぞれの細かい説明は省きます。

カスタムタスク

MSBuild が解析したvcxproj の情報を取得するには、C# でカスタムタスクというものを作成してvcxproj に組み込みます。

カスタムタスクは、NuGet で取得できるMicrosoft.Build パッケージ、Microsoft.Build.Utilities.Core パッケージに含まれるMicrosoft.Build.Utilities.Task クラスを継承したクラスがその本体です。例えば以下のようなものです。

using Microsoft.Build.Utilities;

namespace MyCustomTask
{
    public class MyCustomTask : Task
    {
        // ソースコードファイル名
        public string SourceFiles { get; set; }
        // インクルードパス
        public string IncludePaths { get; set; }
        // プリプロセッサマクロ定義
        public string PreprocessorMacro { get; set; }

        // 実際の処理
        public override bool Execute()
        {
            // 解析処理
            // 成功したらtrue を返す
            return true;
        }
    }
}

プロパティはMSBuild のプロジェクトとパラメータを受け渡すのに使用します。

Execute はカスタムタスクの処理を記述するメソッドでMSBuild から呼び出されます。

これをC# のクラスライブラリプロジェクトとしてビルドしてdll ファイルを生成します。

vcxproj への組み込み

カスタムタスクとはカスタムしたMSBuild のタスクですが、タスクというのはMSBuild が処理するプロジェクト(vcxproj やcsproj) に含まれる、実際の処理の単位です。

MSBuild のタスクはプロジェクトファイルの中にあるターゲットに含まれます。ターゲットというのはタスクをまとめたもので、処理の起点として指定したり、処理同士の依存関係や処理順序を制御する役割があります。既定のターゲットとしてBuild やClean などがあります。

作成したカスタムタスクは、MSBuild プロジェクトにUsingTask 要素で追加して、独自ターゲットから呼び出します。MSBuild プロジェクトへの追加は、vcxproj ファイルを直接編集することもできますが、vcxproj と同じディレクトリにDirectory.Build.props やDirectory.Build.targets などのファイルを置いておくと自動的にインポートされます。これらはvcxproj と同様のxml 形式のファイルですが、Directory.Build.props はvcxproj の内容より前にインポートされて、Directory.Build.targets はvcxproj の内容より後にインポートされるという違いがあります。次のコードはDirectory.Build.targets の例です。

<Project>
  <!-- vcxproj と同じディレクトリにあるMyCustomTask.dll のタスクを使用 -->
  <UsingTask
    TaskName="MyCustomTask.MyCustomTask"
    AssemblyFile="MyCustomTask.dll" />
  <!-- 独自ターゲット -->
  <Target
    Name="MyCustomTarget"
    BeforeTargets="ClCompile">
    <!-- カスタムタスクを呼び出す -->
    <MyCustomTask
      SourceFiles="@(ClCompile)"
      IncludePaths="$(IncludePath);%(ClCompile.AdditionalIncludeDirectories)"
      PreprocessorMacro="%(ClCompile.PreprocessorDefinitions)"
      />
  </Target>
</Project>

UsingTask で使用するタスクの名前とタスクが含まれるアセンブリを指定しています。タスク名は名前空間も含めたものです。Target の子要素として呼び出すときは名前が衝突しなければ名前空間は省略できます。

Target 要素ではName 属性で任意の名前を指定しています。BeforeTargets 属性は、このMyCustomTarget がどのターゲットより前に処理されるかという依存関係を表します。指定されているClCompile ターゲットは、MSBuild でのC++コンパイルを行うターゲットです。つまりこの例はMyCustomTarget ターゲットに含めたMyCustomTask がC++コンパイルの前に呼び出されるような指定となります。

カスタムタスククラスへの入力

カスタムタスククラスのプロパティにパラメータを渡す場合、MSBuild プロジェクトではタスク要素の属性として指定します。前のDirectory.Build.targets の例の場合を順に説明します。

まず入力ソースファイルを受け取るプロパティのSourceFiles には@(ClCompile) を渡していますが、これはvcxproj の中を覗くと見つけられるItemGroup の子要素となっているClCompile をまとめたものです。MSBuild の用語ではItemGroup の子要素は項目といいます。@(ClCompile) はカスタムタスクのC# 側クラスのプロパティにはセミコロンで連結されたファイルパスとして文字列で渡されます。C# 側ではそれを分割すればファイルパスリストに戻せるというわけです。

また、MSBuild 側で@(ClCompile) の代わりに%(ClCompile.Identity) を指定すると、ファイルパスひとつずつに対してカスタムタスクが実行されます。この場合C# 側が受け取るのは単一のファイルパスになります。

インクルードパスを受け取るIncludePaths には$(IncludePath) と%(ClCompile.AdditionalIncludeDirectories) をセミコロンで連結して渡しています。$(IncludePath) はプロジェクト全体に指定されたインクルードパスを保持するプロパティです。SDK ごとのパスなども含まれています。

%(ClCompile.AdditionalIncludeDirectories) は追加のインクルードパスを保持するメタデータです。Visual Studio IDE でプロジェクトのプロパティから指定できる 追加のインクルードディレクトリが入力されますが、ソースファイル個別のプロパティで追加のインクルードディレクトリが上書きされている場合はそちらに置き換わります。C# 側への入力がどうなるかというと、同じ値を持つClCompile 要素ごとに分けて呼び出されます。これは項目のメタデータバッチ処理という機能によるものです。

最後にプリプロセッサマクロ定義を受け取るPreprocessorMacro には%(ClCompile.PreprocessorDefinitions) が渡されています。これもメタデータで、%(ClCompile.AdditionalIncludeDirectories) と同様に処理されます。

なお、目的の情報がMSBuild 内の項目、メタデータ、プロパティなどのどこから取得できるのかはvcxproj をMSBuildプリプロセスし、インポートされるプロジェクトがすべて展開された状態のファイルを出力して、その中から探しました。ほかに必要な情報がある場合も同様に調べることが可能です。

これで、カスタムタスククラスにソースコードのファイルパスと必要な情報を渡すことができるようになりました。解析はlibclang なりClangSharp なり、お好みのライブラリ、言語で自由にやっちゃってください。

生成したソースコードをビルドに含める

なお、カスタムクラスでC++ ソースコードを生成した場合、それをそのままビルドに含めることもできます。

using Microsoft.Build.Utilities;

namespace MyCustomTask
{
    public class MyCustomTask : Task
    {
        // 略

        // MSBuild に値を返すプロパティ
        [Output]
        public string GeneratedFiles { get; set; }

        // 実際の処理
        public override bool Execute()
        {
            // 解析処理は省略
            GeneratedFiles = "path/to/generated.cpp";
            return true;
        }
    }
}
<Project>
  <!-- 略 -->
  <Target
    Name="MyCustomTarget"
    BeforeTargets="ClCompile">
    <!-- カスタムタスクを呼び出す -->
    <MyCustomTask
      SourceFiles="@(ClCompile)"
      IncludePaths="$(IncludePath);%(ClCompile.AdditionalIncludeDirectories)"
      PreprocessorMacro="%(ClCompile.PreprocessorDefinitions)">
      <!-- 出力プロパティの値をClCompile 項目に追加 -->
      <Output TaskParameter="GeneratedFiles" ItemName="ClCompile" />
    </MyCustomTask>
  </Target>
</Project>

C# 側のクラスでOutput 属性を持つプロパティに持たせた値を、MSBuild プロジェクト側ではタスク要素の子のOutput 要素で追加先としてItemName にClCompile を指定するだけです。


C++ のコードは出てきませんでしたが間違いなくC++ プログラミングに役立つものと信じて書きました。

MSBuild のカスタムタスクでファイルを生成してビルドに含める

vcxproj に含まれる情報を取得してlibclang で処理する方法を調べていますが 今回は生成したファイルをビルドに含める方法について説明します。

前回:MSBuild でvcxproj に独自の処理を追加する - 見切り発車

独自の処理を作成する

前回の記事では、独自の処理をMSBuild のビルドプロセスに組み込む方法について 独自ターゲットを追加して既定のターゲットとの依存関係を設定するということを 書きました。

独自ターゲット内で実行される独自処理を作成する場合は、カスタムタスクを作成します。

カスタムタスクはMicrosoft.Build.Framework パッケージに含まれる ITask インターフェースを実装したクラスです。Microsoft.Build.Utilities.Core パッケージに含まれるTask クラスを継承してExecute メソッドをオーバーライドすると 手軽に実装が可能です。

これらのパッケージは、NuGet でインストールできます。

カスタムタスクを作成する方法は、以下のリンクにチュートリアルが用意されています。

https://learn.microsoft.com/ja-jp/visualstudio/msbuild/tutorial-custom-task-code-generation

その中で触れられていますが、フレームワークはnetstandard2.0 にする 必要があるそうです。そのため、Microsoft.Build のNuGet パッケージも 最新の17.x.x ではなく15.x.x を選択しました。

ごく単純なカスタムタスククラスは次のようになります。

import Microsoft.Build.Utilities;

namespace MyCustomTask
{
    public class MyCustomTask : Task
    {
        public override bool Execute()
        {
            return true;
        }
    }
}

このカスタムタスクは何もしませんが、ビルドプロセスからExecute メソッドが 呼び出されます。true を返すと処理が正常終了したことを表します。 false を返すとビルドが失敗します。

カスタムタスクをvcxproj に組み込む

カスタムタスクのクラスを含むプロジェクトは、クラスライブラリとして作成します。

ビルドするとdll が出力されるので、これをvcxproj からUsingTask 要素で参照します。

先ほどのMyCustomTask を参照する場合にはUsingTask 要素は次のように指定します。

  <UsingTask
    TaskName="MyCustomTask.MyCustomTask"
    AssemblyFile="dll ファイルのパス"
    />

TaskName にはタスククラスの名前を入力しますが、名前空間を含めたほうが確実です。

AssemblyFile にはdll のパスを入力しますが、相対パスでもフルパスでもどちらでも大丈夫です。

UsingTask 要素を追加したら、ターゲットで次のようにして呼び出せます。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask />
  </Target>

カスタムタスクをNuGet パッケージ化すると組み込みが簡単になりそうですが それについては別の機会にします。

vcxproj とカスタムタスクのパラメータのやりとり

カスタムタスク側は、プロパティを使用してパラメータを受け取ります。 またカスタムプロセスから情報を返す場合にもプロパティを使用します。

プロパティには属性を指定して、必須項目や出力用として扱います。

public class MyCustomTask : Task
{
    // 必須の入力ファイル名
    [Required]
    public string InputFile { get; set; }

    // オプションのサフィックス
    public string Suffix { get; set; }

    // MSBuild に返す出力ファイル名
    [Output]
    public string OutputFile { get; set; }

    public override bool Execute()
    {
        // 入力ファイルが無ければ失敗
        if (!System.IO.File.Exists(InputFile))
        {
            return false;
        }
        // ファイルパスをディレクトリ、ファイル名、拡張子に分割
        string dir = System.IO.Path.GetDirectoryName(InputFile);
        string fileName = System.IO.Path.GetFileNameWithoutExtension(InputFile);
        string ext = System.IO.Path.GetExtension(InputFile);
        // Suffix の指定が無ければ.gen を付与
        if (string.IsNullOrEmpty(Suffix))
        {
            OutputFile = System.IO.Path.Combine(dir, $"{fileName}.gen{ext}")
        }
        // ファイル名と拡張子の間にSuffix を挿入
        else
        {
            OutputFile = System.IO.Path.Combine(dir, $"{fileName}{Suffix}{ext}")
        }
        // ダミーのコードを出力
        System.IO.File.WriteAllText(OutputFile, "namespace { int _{ 0 }; }");

        return true;
    }
}

vcxproj では入力パラメータをカスタムタスク要素の属性で渡します。 カスタムタスクの出力パラメータの受け取りは後述します。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask
      InputFile="%(ClCompile.Identity)"
      />
  </Target>

InputFile に@(ClCompile) を渡した場合には、C++コンパイル対象の ファイル名がセミコロンで連結されたひとつの文字列が入力となりますが、 %(ClCompile.Identity) を指定すると各入力ファイルごとに分割されて、 それぞれの入力ファイル名ごとにカスタムタスクが呼び出されます。 これはMSBuildメタデータバッチ処理という機能によるものです。

生成したファイルをC++ のビルドに含める

独自のターゲットでClCompile という項目にファイル名を追加すると、 そのファイルもC++コンパイル対象になります。

カスタムタスクで生成したファイルをClCompile に追加したい場合は Output という子要素とカスタムタスクの出力パラメータを利用します。

次の例のようにOutput 子要素のTaskParameter に出力パラメータ名、 ItemName にClCompile を指定すると、カスタムタスクのExecute メソッドで プロパティに設定した内容がClCompile に追加されます。

  <Target Name="MyTarget" BeforeTargets="ClCompile">
    <MyCustomTask InputFile="%(ClCompile.Identity)">
      <Output TaskParameter="OutputFile" ItemName="ClCompile">
    </MyCustomTask>
  </Target>

まとめ

MSBuild のカスタムタスクでC++ のコードを生成してビルドに含める方法について説明しました。

今回の内容も、チュートリアルとサンプルプロジェクト、 公式のドキュメントの情報を参照して調べたものです。 時間をかけて読むのは強いなと思いました。

MSBuild でvcxproj に独自の処理を追加する

vcxproj からソースファイル名やインクルードパス、プリプロセッサマクロ定義などを取り出してlibclang に渡して処理するという目的のため試行錯誤しているのですが、 断片的な情報でハックするようなやり方ではなかなか思ったような結果にならないため改めてMSBuild のドキュメントに記載されている公式に提供されている手段で実現する方法を調べました。

今回は特にvcxproj のビルドプロセスに独自の処理を追加する方法と、コンパイラに渡されるパラメータを利用する方法について記述します。

MSBuild のドキュメント

https://learn.microsoft.com/ja-jp/visualstudio/msbuild/msbuild?view=vs-2022

MSBuild についての情報はこちらに記載されています。 今回の内容はこちらに記載されているものとGitHub で提供されているサンプルを参考にしました。

基本的にはMSBuild のドキュメントをしっかり読み込むことができればほぼすべての情報は得られると思います。

独自ターゲットをビルドプロセスに組み込む

vcxproj などのMSBuild のプロジェクトファイルでは各種の処理を行うタスク要素をターゲット要素の子要素としてまとめています。 そしてMSBuild の実行時に起点となるターゲットを指定すると依存関係のあるターゲットに含まれるタスクが実行されます。

独自の処理を追加したい場合、C# で独自のタスククラスを含むクラスライブラリを作成しこれを呼び出す独自ターゲットを定義して既定のターゲットとの依存関係を設定します。

例えば、vcxproj でコンパイルを行うターゲットはClCompile という名前ですが、 コンパイル処理の前に独自の処理を実行したい場合、独自のターゲットを作成してBeforeTargets 属性にClCompile を指定します。

<Target Name="MyPreClCompile" BeforeTargets="Clcompile">
  <Message Text="MyPreClCompile" />
</Target>

コンパイラに渡される情報を利用する

MSBuild には項目、メタデータ、プロパティといったパラメータがあります。

項目はItemGroup で定義される主にファイルを指すパラメータです。 @(ClCompile) のように@ を付けて参照します。

メタデータはItemGroup やItemDefinitionGroup で項目の子要素として定義されます。 これには%(ClCompile.PreprocessorDefinitions) のように% を付けて項目の子要素として参照します。

プロパティはPropertyGroup で定義されるパラメータです。 $(IncludePath) のように$ を付けて参照します。

ClCompile でどのようなメタデータが利用できるかは C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Microsoft\VC\v170 などのディレクトリから探すこともできますが、 vcxproj をMSBuild の/pp オプションでプリプロセスして出力したファイルから読み取ることもできます。

独自ターゲットでMessage タスクを利用して各パラメータの値を確認することができます。

<Target Name="MyPreClCompile" BeforeTargets="Clcompile">
  <Message Text="ClCompile=@(ClCompile)" />
  <Message Text="IncludePath=$(IncludePath)" />
  <Message Text="PreprocessorDefinitions=%(ClCompile.PreprocessorDefinitions)" />
</Target>

パラメータの種類に合わせた参照するための記号を使用するよう注意が必要です。

独自ターゲットの挿入位置

プロパティやメタデータは同じ要素がvcxproj 内の複数個所にある場合には出現順序によって最終的な結果が決まるため、 ターゲットでこれらを定義している場合には順序に気を付ける必要がありますが、 今回のように参照するだけであれば他のターゲットとの前後関係はあまり気にしなくてよさそうです。 vcxproj に直接挿入する以外に、vcxproj と同じ階層に置いたDirectory.Build.targets ファイルに記述する方法もあり、 こちらのほうが独立して取り扱いがしやすいかもしれません。

まとめ

vcxproj に独自処理を追加してvcxproj の情報を利用する場合には独自ターゲットを追加して既定ターゲットとの依存関係を指定し、 種類ごとに決められた記号を使用してパラメータの値を参照します。

これらはMSBuild のドキュメントに書いてあり、時間はかかりますが理解しておくと目的に合わせたアレンジがしやすくなりそうです。

ClangCL のexe の差し替えの続き

2022-12-01 追記: 後から調べた以下の記事のほうがまっとうな手法です。 uyamae.hatenablog.com


Visual Studio のClangCL ツールセットのコンパイラを差し替える - 見切り発車

Visual Studio 標準のClang/LLVM のカスタマイズ方法を試す - 見切り発車

libclang に渡すコンパイルオプションやインクルードパスなどの情報をvcxproj から取り出そうとほそぼそと手探りで進めていますが、ネット上に散らばった情報をつまんでうろうろした結果として公式に用意されているMSBuild のドキュメントを頭から読んだほうがいいよなやっぱり、となりました。うすうす気づいてはいました。

ビルドプロセスのカスタマイズについては

ビルドのカスタマイズ - MSBuild | Microsoft Learn

この辺りで説明されていますが、おおむねprops ファイルは標準的なプロセスの前にインポートされ、targets ファイルはあとからインポートされる、というようなことが書かれています。

前にDirectory.Build.props でClangCL.exe を差し替えようと試したけどうまくいきませんでしたが、Directory.Build.targets で指定してやると反映されるようです。Github においてるテストプロジェクトも更新しました。

clang-hook/makeproj.ps1 at master · uyamae/clang-hook · GitHub

名前からしてprops にはプロパティ、targets にはターゲットを指定するものかと思いましたがそう単純というか厳密なものではないようです。

MSBuild API を試している途中

MSBuild から呼び出されるコンパイラの実行ファイルを差し替えてコンパイルに必要な情報を横取りしてやろうと思っていましたがインクルードパスが渡されてこないようなのでもう少しMSBuild を掘り下げてみることにしました。

MSBuildMicrosoft.Build というパッケージを導入することで利用できるようです。実行環境導入が手軽になるかなとの考えで、PowerShell スクリプトからの利用を試しましたがちょっと知識と情報が不足していてうまくいかず。

なかなか進みそうもないのでC# にNuGet でパッケージを導入する方向へ軌道修正。それでも、Microsoft.Build.Utirities.Core パッケージは別途手動でインストールが必要など、なかなか手ごわい感じです。

いろいろググっている中でヒットしたこちらは勉強になりました。

NuGetでビルドプロセスに介入するパッケージを作る – kekyoの丼

もう少しどうにかなったらまとめたい。

Visual Studio 標準のClang/LLVM のカスタマイズ方法を試す

概要

Microsoft 公式の「Visual Studio プロジェクトでの Clang/LLVM のサポート」によるclang-cl.exe の差し替えができるか試しました。

リンク先の内容が難なく理解できるのであれば問題ありませんがところどころ明確でない点があったので書きとめます。

試したこと

Visual Studio 2019 バージョン 116.9 以降ではLLVM のカスタムツールセットのバージョンを設定できるそうです。

Visual Studio 2019 や2022 ではインストール時に個別のコンポーネントとして「v143 ビルドツール用C++ Clang-cl」や「WindowsC++ Clang コンパイラ」などを選択しているとVisual Studio の一部としてLLVM/clang がインストールされます。

しかし、LLVMGithub で提供されているものを利用したい場合にはvcxproj やsln と同じ階層にDirectory.build.props というファイルを作成して置いておくという方法で対応が可能というものです。

例として挙げられているDirectory.build.props の内容は以下のようになっています。

<Project>
  <PropertyGroup>
    <LLVMInstallDir>C:\MyLLVMRootDir</LLVMInstallDir>
    <LLVMToolsVersion>15.0.0</LLVMToolsVersion>
  </PropertyGroup>
</Project>

今回、自分の環境にhttps://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.0 から取得したLLVM-16.0.0-win64.exe でLLVM をインストールしてみたのでそちらの利用を試してみます。

やってみる

まずLLVMInstallDir ですが、LLVM-16.0.0-win64.exe でのデフォルトのインストール先は「C:\Program Files\LLVM」となっていたのでこれを指定します。

LLVMToolsVersion にはカスタム LLVM ツールセットバージョンを指定するのですがインストール先のC:\Program Files\LLVM 直下にはバージョンごとのディレクトリは見当たりません。ひとまずインストールしたバージョンの16.0.0 を指定してみます。

<Project>
  <PropertyGroup>
    <LLVMInstallDir>C:\Program Files\LLVM</LLVMInstallDir>
    <LLVMToolsVersion>16.0.0</LLVMToolsVersion>
  </PropertyGroup>
</Project>

ビルドを実行してみると以下のようにエラーとなりました。

  C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Microsoft\VC\v170\Microsoft.Cpp.ClangCl.Common.targets(74,5): error MSB8073: LLVM ツールセット バージョン '16.0.0' が見つかりません: フォルダー ''C:\Program Files\LLVM\lib\clang\16.0.0'' が存在しません。LLVM ツールセット バージョン '16.0.0' がインストールされていることを確認するか、別のツールセット バージ ョンを選択してください。

C:\Program Files\LLVM\lib\clang を見てみると「16」というディレクトリが存在しています。そこでDirectory.build.props のLLVMToolsVersion の値を「16」に変更してみたところビルドができました。

Directory.build.props でCLToolExe の差し替えを試す

前回 はvcxproj を編集してclang-cl.exe の代わりに自作のプログラムが呼び出されるようにしましたが、Directory.build.props で変更できるかどうか試してみました。

<Project>
  <PropertyGroup>
    <LLVMInstallDir>C:\Program Files\LLVM</LLVMInstallDir>
    <LLVMToolsVersion>16.0.0</LLVMToolsVersion>
    <CLToolExe>clang-hook.exe</CLToolExe>
    <ExecutablePath>$(SolutionDir)hook\$(Configuration);$(ExecutablePath)</ExecutablePath>
  </PropertyGroup>
</Project>

しかし、clang-hook.exe は呼び出されませんでした。Directory.build.props でCLToolExe を指定しても反映されないようです。

Visual Studio のClangCL ツールセットのコンパイラを差し替える

動機

そもそもの動機は、libclang でソースコードを解析するときに渡すオプションの類をVisual Studio のvcxproj から引っ張り出せないか、というものです。

vcxproj はxml 形式のテキストファイルで、中を覗くとコンパイルオプションなどが含まれているのがわかりますが、Import でどこからともなく別ファイルを取り込んできたり$VCInstallDir のようなどこで定義されているのか分からないプロパティ(環境変数?)があったり、Condition 属性であまり馴染みのない高度な機能を持っているっぽい条件式を評価する必要があったりと、解析するのはなかなか大変です。

MSBuild でうまいことやる方法とか無いものか、と考えつつ試行錯誤していく中で、ふと「Visual Studio がclang に渡しているであろう引数を、clang のふりをして受け取ってしまえばよいのでは?」と思いつきました。

そこで「visual studio clang 自作」でググってみたところ、Visual Studioでも最新のClangが使いたい! という記事が見つかりました。

内容はMS のclang に渡されるオプションを公式のclang に渡すためにツールセットを自作する、ということのようです。やろうとしていることにとても近いので大いに参考にさせていただくことにしました。

しかし、対象となっているVisual Studio のバージョンが2017 なので現在利用できる2022 ではそのまま使えないところもあるため独自の調査も必要そうです。

ひとまず呼び出されるコンパイラを差し替えてみよう、というのが今回試した内容です。

とっかかり

ツールセットのフォルダに含まれるToolset.props, Toolset.targets を独自に編集することでどうにかできるということはわかりましたが何をどうすればいいのかがはっきりしません。

そこで、vcxproj の中身を展開してみます。ツールセットにClangCL(LLVM clang-cl) が指定されたvcxproj をMSBuildプリプロセスしてやるとImport 要素をすべて取り込むことができます。

> MSBuild /pp:test.pp.vcxproj test.vcxproj

/pp オプションに出力ファイル名を指定しますがこれは任意でよさそうです。またMSBuild のパスが通っていない場合はVisual Studio の開発者用コンソールを使用するとよいです。

出力されたファイル内を探すと、clang-cl.exe の名前が見つかります。

  <PropertyGroup>
    <TargetPlatformIdentifier>Clang.Windows</TargetPlatformIdentifier>
    <ToolsetISenseIdentifier>$(TargetPlatformIdentifier)</ToolsetISenseIdentifier>
    <CLToolExe>clang-cl.exe</CLToolExe>
    <LinkToolExe>lld-link.exe</LinkToolExe>
    <LibToolExe>llvm-lib.exe</LibToolExe>
    <!-- 略 -->
    <ExecutablePath>$(LLVMInstallDir)\bin;$(ExecutablePath)</ExecutablePath>
    <!-- 略 -->
  </PropertyGroup>

今のところ詳細は不明ですが、CLToolExe という要素でコンパイラを指定しているようです。また、CLToolExe にはexe ファイル名だけが指定されていてパスはExecutablePath 要素で指定しているようです。このふたつを変えてどうなるのか見てみます。

やってみる

今回試してみたものをgithub に上げました。

github.com

まず、clang-cl.exe と置き換えるものとしてclang-hook.exe というプログラムを用意しています。これは受け取った起動引数を標準出力に表示してから終了コード1 で終了します。コンパイラを置き換えているのにオブジェクトファイルを出力していないのでそこでエラーとしてリンクに進まないようにするためです。今回はコンパイラを差し替える方法の調査が目的なのでこの先の処理のことは後で考えます。

次にclamg-hook.exe を適用する対象としてtest というプロジェクトを用意しています。中身は何でもよいです。

CLToolExe, ExecutablePath の変更は参考先の記事ではツールセットを自作して実現していますが、今回はCMake で生成したvcxproj を後行程で加工するようなPowerShellスクリプトを書いています。実行してみるとtest.vcxproj の末尾にPropertyGroup を追加します。

  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
  <PropertyGroup>
    <CLToolExe>clang-hook.exe</CLToolExe>
    <ExecutablePath>$(SolutionDir)hook\$(Configuration);$(ExecutablePath)</ExecutablePath>
  </PropertyGroup>
</Project>

CLToolExe, ExecutablePath は直接vcxproj に含まれてはいませんが最後に上書きしてやろうというねらいです。

結果

スクリプトを実行し生成されたclang-hook.sln をVisual Studio で開いてビルドしてみると、出力にclang-hook.exe がエラーを返した旨が表示されます。また、clang-hook.exe には引数としてclang-cl に渡すためのオプションをまとめた.rsp ファイルのパスが渡されているのもわかります。

このようにコンパイラの差し替えはわりと単純に実現できそうなことがわかりました。素敵な仕組みのようです。