BooでAOP

今回はBooでAOPアスペクト指向プログラミング)的なことをやってみる。

Booでアスペクト(のようなもの)をコードに織り込む場合はコンパイラのプロセスに直接干渉してやることになる。
他のAOPライブラリ、例えばPostSharpなんかだとコンパイルした後にILをいじくったり、他にはRealProxyを使ってプロキシをかましたりとかあるけど、Booでは標準でコンパイラのプロセスに割り込むためのパイプラインという機構が用意されている。

この機構を使うには「AbstractVisitorCompilerStep」クラスを利用する。

このクラスを継承して「CompileToFile」クラスを継承したクラスのコンストラクタでコンパイラステップとして追加する。

その例が以下のようになる。

sample.boo
namespace Sample

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Steps
import Boo.Lang.Compiler.Pipelines

# 何もしない
class SamplePipelineStep(AbstractVisitorCompilerStep):
    override def Run():
        self.Visit(self.CompileUnit)

class SamplePipeline(CompileToFile):
    def constructor():
        self.Insert(1, SamplePipelineStep())

これの使い方は、まずこのファイルをDLLとしてコンパイルする。

PS > booc.exe /t:library sample.boo

そしてこのパイプラインを適用したいソース「main.boo」というのがあるとして、このソースをコンパイルする時に以下のようなコマンドラインオプションを指定する。

PS > booc.exe main.boo -r:sample.dll -p:"Sample.SamplePipeline, Sample"

「-p」でパイプラインクラスのクラス名とアセンブリ名を指定してやる。

このようにコンパイルすることで「main.boo」のコードに対してパイプラインで指定した処理が行われることになる。

正直ここまで書いていて自分でも何を書いているのかわからなくなってきたので、実例を紹介してみる。

DbC的なもの

DbC(Design by Contract)的なものとして、メソッドの引数にnullが渡されたら例外を投げるようなパイプラインを作ってみた。

DbC.boo

namespace Sample.DbC

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Steps
import Boo.Lang.Compiler.Pipelines

class DbCPipelineStep(AbstractVisitorCompilerStep):
    override def Run():
        self.Visit(self.CompileUnit)

    override def LeaveModule(module as Module):
        // import System
        module.Imports.Add(Import(Namespace: "System"))
        
    override def LeaveMethod(method as Method):
        for p in method.Parameters:
            block = Block()
            block.Add(RaiseStatement(
                // raise ArgumentNullException()
                MethodInvocationExpression(
                    ReferenceExpression("ArgumentNullException")
                )
            ))
            // if arg == null:
            ifSt = IfStatement(
                BinaryExpression(BinaryOperatorType.Equality,
                    ReferenceExpression(p.Name), NullLiteralExpression()
                ),
                block, null
            )
            method.Body.Insert(0, ifSt)

class DbCPipeline(CompileToFile):
    def constructor():
        self.Insert(1, DbCPipelineStep())

「LeaveMethod」メソッドで引数の数だけ、ifステートメントで引数をnullと比較してtrueなら「ArgumentNullException」を投げている。

そして、このパイプラインを適用するソースが以下。

main.boo
class Hoge:
    def Hello(msg as string):
        print "hello, ${msg}"

obj = Hoge()
obj.Hello("world")
obj.Hello(null)

コンパイルするためのコマンド

PS > booc.exe "-t:library" DbC.boo "-o:Sample.DbC.dll" -debug- -nologo
PS > booc.exe main.boo "-r:Sample.DbC.dll" "-p:Sample.DbC.DbCPipeline, Sample.DbC" -debug- -nologo

これで「main.exe」を実行すると二つ目のメソッド呼び出しで例外が出ることになる。

今回の例では「main.boo」で定義されたクラスのメソッドの呼び出し全てに対して、このチェックが行われるけど実際にはカスタム属性を作って、それが付与された引数だけにチェックを行うとかにする必要がある。

なんか面倒くさいけど、このパイプラインはメソッド呼び出しだけでなく、ありとあらゆるコード?に対して割り込むことができるので、かなりいろんなことができると思う。

あと、この「main.exe」を実行するにはカレントディレクトリに「Boo.Lang.dll」が必要なんだけど、コピーしてくるのが面倒くさかったのでアプリケーション構成ファイルを作ってAssemblyBindingを設定しておいた。

以下がその構成ファイル

main.exe.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="Boo.Lang"
                              publicKeyToken="32c39770e9a21a67"
                              culture="neutral" />
            <codeBase version="2.0.0.0"
                      href="file:///d:/Program Files/Boo/bin/Boo.Lang.dll"/>
         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>

ソース