nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

XamarinでもAOPしたい! Fody&Mono.Cecil編

さて前回、Mono.Cecilを利用した静的なAOPについて解説しました。

今回はいよいよXamarin.iOSでも動作する、静的なAOPの実装について解説したいと思います。Mono.Cecilを使ってILを織り込むFodyのアドインを作成し、コンパイル時に自動的にILを織り込むところまでを解説します。

具体的にはViewModelのメソッド呼び出し時にDebug.WriteLineでクラス名とメソッド名をログ出力するサンプルを作成します。

「使い勝手の良いAOPフレームワーク」は、まだまだお預けですが、今回を理解すればあとはILを「頑張れば」できたも同然でしょう(つまり先はまだ長いです)。

では早速内容に入って行きましょう。

ログを織り込み対象のXamarinアプリを作成する

まずはテスト対象のXamarinアプリをサクッと作りましょう。

こんな感じのボタンとラベルが表示されていて、ボタンをクリックするとラベルに表示されているカウントがインクリメントされていくだけの、シンプルなアプリケーションです。

f:id:nuitsjp:20171212154031g:plain

ViewModelにはCountUpメソッドが定義されており、ボタンにバインドされているCountUpCommandが実行されるとCountプロパティがインクリメントされるよう実装されています。

public class MainPageViewModel : INotifyPropertyChanged
{
    private int _count;
    public int Count
    {
        get => _count;
        set => SetProperty(ref _count, value);
    }

    public ICommand CountUpCommand => new Command(CountUp);

    private void CountUp()
    {
        Count++;
    }

詳細なコードはGithubに公開していますのでこちらをご覧ください。

github.com

こちらのXFodyAppプロジェクトを参照してください。

Fodyアドインを作成する

それではここから、実際にILを編み込むFodyのアドインを作成します。
まずは「Hello.Fody」と言う名称の.NET Standard 1.3クラスライブラリを作成し(Mono.Cecilに合わせています)、NuGetからFodyCecilを適用しましょう。

f:id:nuitsjp:20171212161605p:plain

Fodyのアドインは「〜.Fody」と言う命名規則で作成する必要があります。 またアドインはクラス名も決められています。「ModuleWeaver」と言う名称で「ルート名前空間上に」作る必要があります。

さて、アドイン処理はExecuteメソッドに実装していきます。またModuleDefinitionプロパティを必ず定義する必要があります。
次のコードがミニマムな実装になります。何もIL操作していませんが。

using Mono.Cecil;

public class ModuleWeaver
{
    public ModuleDefinition ModuleDefinition { get; set; }

    public void Execute()
    {
    }
}

ModuleDefinitionプロパティに、アスペクトを編み込む対象のアセンブリ情報がFodyから設定された上で、Executeメソッドが呼び出されます。

さて、ILを編集するコードはサクッとこんな感じで実装します。

using System.Linq;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;

public class ModuleWeaver
{
    public ModuleDefinition ModuleDefinition { get; set; }

    private MethodInfo DebugWriteLine { get; } =
        typeof(System.Diagnostics.Debug)
            .GetTypeInfo()
            .DeclaredMethods
            .Where(x => x.Name == nameof(System.Diagnostics.Debug.WriteLine))
            .Single(x =>
            {
                var parameters = x.GetParameters();
                return parameters.Length == 1 &&
                       parameters[0].ParameterType == typeof(string);
            });

    public void Execute()
    {
        var methods = ModuleDefinition
            .Types.Where(x => x.Name.EndsWith("ViewModel"))
            .SelectMany(x => x.Methods);
        foreach (var method in methods)
        {
            var processor = method.Body.GetILProcessor();
            var current = method.Body.Instructions.First();

            processor.InsertBefore(current, Instruction.Create(OpCodes.Nop));
            processor.InsertBefore(current, Instruction.Create(OpCodes.Ldstr, $"DEBUG: {method.DeclaringType.Name}#{method.Name}()"));
            processor.InsertBefore(current, Instruction.Create(OpCodes.Call, ModuleDefinition.ImportReference(DebugWriteLine)));
        }
    }
}

名称が「ViewModel」で終わるクラスの全てのメソッド呼び出し時に、クラス名とメソッド名をログ出力するようILを織り込んでいます。
今回は詳細な解説は省きます。この辺りの詳細は前回のエントリーをご覧ください。

これでFodyアドインの実装は完了しました。

アドインをNuGetパッケージ化する

さてアドインを適用するためには、ライブラリをNuGetパッケージ化する必要があります。
これにはFodyの仕様上の問題なのですが、Fodyの設定ファイルに記載されているアドインのアセンブリをロードする際、Fodyのアセンブリが展開さているローカルファイルシステム上の、Fodyと兄弟関係にあるフォルダを相対的に探しに行くからです。

このため、FodyはNuGetから適用するでしょうから、FodyのアドインもNuGetパッケージ化する必要があります(無理やり手で置いても動作しますが)。

通常、.NET StandardのクラスライブラリはVisual Studioのプロジェクトプロパティのパッケージタブで「構築時にNuGetパッケージを生成する」にチェックするだけで簡単にNuGetパッケージを作成できます。

f:id:nuitsjp:20171212164954p:plain

しかし、この方法で作成したFodyアドインのNuGetパッケージは、正しく動作しません。

なぜダメかと言いますと、作成したアセンブリ(dll)がNuGetパッケージ内のどこに配置されているか、Fodyが期待するパスと異なるパスに配置されてしまい、Fodyが発見できないためです。
Visual Studioのパッケージタブの「構築時にNuGetパッケージを生成する」から作成した.NET Standardのnupkgファイルを展開すると、次のようなフォルダ構成になっています。

f:id:nuitsjp:20171211162951p:plain

dllはサブディレクトリである「lib¥netstandardX.X」フォルダに配置されます。

しかしFodyはnupkgファイルを展開した直下にdllが配置されていることを期待します。

f:id:nuitsjp:20171211163426p:plain

これはおそらく、Fody側の不適切な(と言うより.NET Standard以前の)仕様のためだと思いますが、(きっとそのうち解消されるでしょうけど)仕方ないのでパッケージは手で作成しましょう。

適当なフォルダにHello.Fody.nuspecと言う名称でテキストファイルを作成し、次のように記述します。

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>Hello.Fody</id>
    <version>1.0.0</version>
    <authors>Atsushi Nakamura</authors>
    <owners>Atsushi Nakamura</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>Package Description</description>
    <dependencies>
      <dependency id="Fody" version="[2.2.1, 3.0.0)" />
    </dependencies>
  </metadata>
</package>

Hello.Fody.nuspecファイルを作成したら、同じディレクトリに先ほど作成したアドインのdllとpdbファイルを配置しましょう。

f:id:nuitsjp:20171211164333p:plain

そしてnuget.exeをダウンロードして次のように実行しましょう。

nuget.exe pack Hello.Fody.nuspec

Hello.Fody.nuspecのパスは適宜修正してください。
さぁこれでNuGetパッケージの作成が完了しました。いよいよ次はアスペクトを織り込みます。

ILの編集対象プロジェクトへ、Fodyアドインを導入する

ではアスペクトを織り込む対象のプロジェクトにNuGetパッケージを適用しましょう。
今回はNuGetのサービスサイトにはアップロードせず、ローカルのファイルシステムから適用します。
「NuGetパッケージの管理」を開き、右上の歯車マークをクリックしましょう。

f:id:nuitsjp:20171211165628p:plain

開かれたWindowで、先ほど作成したローカルのパスを指定します。

f:id:nuitsjp:20171211170112p:plain

ローカルパスを登録したら、パッケージソースに作成したものを指定してHello.Fodyパッケージをインストールしてください。

f:id:nuitsjp:20171211170745p:plain

インストールしたら「FodyWeavers.xml」と言うファイルをプロジェクト直下に作成します。

f:id:nuitsjp:20171213004140p:plain

ここに作成したHello.Fodyアドインを適用するように記述します。

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <Hello/>
</Weavers>

.Fodyは削除して記述しましょう。
さあ、あとはビルドして完成です。

f:id:nuitsjp:20171213001516g:plain

ちょっとイメージと違いましたか?
サンプルは「ViewModelのメソッド呼び出し時にDebug.WriteLineでログ出力する」仕様のため、ビルド時にプロパティのSetterやGetterとして生成されているメソッドに対しても、ログ出力が織り込まれていることになります。

最後に

さて、これで.NET Standardクラスライブラリとして作成されたFodyアドインを、.NET StandardのXamarinプロジェクトへ適用することができました。

とは言え、あくまでHello, Worldレベルの話で、AOPライブラリとしては全くの未完成です。しかし、ここまでくればあとは頑張ってILを生成するコードを実装すれば実現できる…はずです。

25日のAdvend CalenderまでにAOPフレームワークっぽいものが完成していることを祈っておいてください。

という訳で今回はここまで。それではまた!