アジョブジ星通信

進捗が出た頃に更新されるブログ。

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 なのでそのままカタカナにしました