ICompileModule でメタプログラミングするやつ
あけましておめでとうございます。今週のお題「今年こそは」だそうですが、今年こそは強い人間になりたいですね。というのも最近どんどん挑戦することが嫌になってきて……。
本題行きます。ASP.NET タグが指定してあることからわかるように、今回も DNX の話です。DNX には C# のコンパイル時にコードを書き換えることができる機能があります。一時期 Yeoman の吐くテンプレートで Razor の事前コンパイルに使われていたようですが、今試したところなくなっていましたね。。というわけでこれの使い方を紹介していきます。
注意
ASP.NET5 RC1-final 時点での情報です。将来 Roslyn 自体にこの機能がぶちこまれて DNX 側の機能ではなくなる可能性もあります。
プリプロセスの仕組み
project.json で preprocess, preprocessExclude, preprocessFiles を指定すると、プリプロセスソースファイル*1として認識され、通常のコンパイル対象から除外されます。また、何も指定しない場合は compiler/preprocess/**/*.cs
が使用されます。
(ここまではDNXの機能です。ここから先は Microsoft.Dnx.Compilation.CSharp の機能なので、コンパイラを自作するときには自分で実装する必要があります。)
プリプロセスソースファイルが存在する場合、C#コンパイラはプリプロセスソースファイルをコンパイルし、コンパイル結果のアセンブリから Microsoft.Dnx.Compilation.CSharp.ICompileModule インターフェイスを実装した public な型を検索し、プロジェクトのコンパイル時に ICompileModule.BeforeCompile, AfterCompile を呼び出します。
BeforeCompile は CSharpCompilation が作成され、ソースファイルの構文解析が終わったタイミングで呼びだされます。
AfterCompile はアセンブリが作成されたタイミングで呼びだされます。
project.json の準備
DNX で動かすので、ターゲットフレームワークとして dnx451 と dnxcore50 を指定しておく必要があります。コンパイル時に NullReferenceException が発生するようだったら大体これが原因です。
また ICompileModule が含まれる Microsoft.Dnx.Compilation.CSharp.Abstractions パッケージへの参照を設定しておきます。
{ "frameworks": { "dnxcore50": { "dependencies": { "Microsoft.Dnx.Compilation.CSharp.Abstractions": { "version": "1.0.0-rc1-final", "type": "build" } } } "dnx451": { "dependencies": { "Microsoft.Dnx.Compilation.CSharp.Abstractions": { "version": "1.0.0-rc1-final", "type": "build" } } } } }
ICompileModule を実装する
compiler/preprocess 下に .cs ファイルを作成します。
ICompileModule を実装したクラスのスケルトンはこのようになります。
using System; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Dnx.Compilation.CSharp; public class MyCompileModule : ICompileModule { public void BeforeCompile(BeforeCompileContext context) { } public void AfterCompile(AfterCompileContext context) { } }
BeforeCompileContext と AfterCompileContext の定義はこのようになっています。
namespace Microsoft.Dnx.Compilation.CSharp { public class BeforeCompileContext { // Roslyn の Compilation オブジェクト public CSharpCompilation Compilation { get; set; } public ProjectContext ProjectContext { get; set; } public IList<ResourceDescriptor> Resources { get; set; } // エラーや警告 public IList<Diagnostic> Diagnostics { get; set; } //参照アセンブリ/プロジェクト public IList<IMetadataReference> MetadataReferences { get; set; } } public class AfterCompileContext { public ProjectContext ProjectContext { get; set; } public CSharpCompilation Compilation { get; set; } public Stream AssemblyStream { get; set; } public Stream SymbolStream { get; set; } public Stream XmlDocStream { get; set; } public IList<Diagnostic> Diagnostics { get; set; } } public class ProjectContext { public string ProjectDirectory { get; set; } public string ProjectFilePath { get; set; } public string Name { get; set; } public FrameworkName TargetFramework { get; set; } public string Configuration { get; set; } public string Version { get; set; } } }
BeforeCompile では Compilation を操作するのが主にやることになると思います。CSharpCompilation はイミュータブルなので作業が終わったら Compilation プロパティにセットするのを忘れないようにしてください。また、MetadataReferences を触っても反映されないので、参照アセンブリの操作は Compilation を直接変更してください。あと、ソースコードを解析して警告を出すといった使い方もできるかもしれません。
AfterCompile では出力されたアセンブリを直接操作することができます。インライン IL とかここで実装すればできそうだな。
実際に使ってみた例
ソースコードを追加してみた例です。
LibAzyotter/RestApisCompileModule.cs at 30a3c2bbe23134a04da009aa687f64988926705c · azyobuzin/LibAzyotter · GitHub
コンパイル時生成なので Visual Studio がそんなクラスないよって怒り出すようになりましたが、僕は元気です。
*1:ProjectFilesCollection.PreprocessSourceFiles なのでそのままカタカナにしました