uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

GroovyConsoleの改善と、その背景にあるワザ(@ThreadInterrupt AST変換)

@tyuki39さんの

のつぶやきを切っ掛けとして、Groovy 1.8のGroovyConsoleを試してみて気づいたのですが、以前(1.7.x)のGroovyConsoleでは、無限ループ、もしくは長時間走行するスクリプトを途中で止めることができないことがありました。
f:id:uehaj:20110416235022p:image
GroovyConsoleには上のように「赤いバツ」のボタンがあって、これを押すと、走行中のGroovyスクリプトのスレッドに対しておそらくThread#interrupt()が発行されるのですが、ご存知のようにJavaにおけるスレッド外からの「スレッド止め」は鬼門であり、安全に止める方法はもともとありませんでした。Java的には「スレッド自身が自らチェックして自ら停止すべき」なのです*1。だから、

while(true);

のようなGroovyコードはThread#interrupt()では止りませんでした*2。これが、Groovy 1.7のGroovyConsoleで赤バツボタンを押てもスクリプトで止らない理由だと推測していました。

でも、Groovy 1.8.0 currentのGroovyConsoleで試したら、上の場合でも難なくInterruptedExceptionが上がって止ります。
f:id:uehaj:20110416235023p:image
Groovy 1.8.0では、@ThreadInterruptというAST変換が導入されていて、これは勝手にisInterrupted()チェックをコードのそこかしこに挿入してくれるというクロマジュツなものなのですが、これを使ってるのかと思ってアタリをつけてソースを見ると、GroovyConsoleの実体であるConsole.groovyには、

    void newScript(ClassLoader parent, Binding binding) {
        def config = new CompilerConfiguration()
        config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt))

        shell = new GroovyShell(parent, binding, config)
    }

のようなところがあり、それっぽいです。ほーこうするとGroovyShellでスクリプトから見て暗黙にAST変換を適用できるのですねえ*3

このAST変換が適用されると、先のwhileループは以下のように変換されて実行されます。

while(true) {
  if (java.lang.Thread.currentThread().isInterrupted()) {
    throw new java.lang.InterruptedException('Execution Interrupted')
  }
}

なお、同種のAST変換である@TimedInterruptはタイムアウト指定、@ConditinalInterruptは任意のクロージャによるチェック条件指定を行うものです。

ちなみにGroovy 1.8のAST Viewerも改良されて、非常にわかりやすくなってますね。AST変換の途中結果が、ASTツリーとしてだけではなく、Groovyコードとしても見えます。

*1:止るように作っておかないなら、それはプログラムのバグなので直すべき、もしくはそういう仕様なので止らないのが正しい。まあ完結したプログラムならそれで良いかもしれませんが、第三者からアップロードされたスクリプトを実行するプログラムとか、ドメインエクスパートが書くDSL断片とか、デバッグ中のプログラムの場合は困ることです。

*2:他の理由で止らないときもあるようです。たとえば大量のデータをprintlnで表示したときなど。これはGUIのイベント処理が追いつかないみたいで、@tyuki39さんの問題はこちだったようでしたが、この場合には1.8.0のGroovyConsoleでも止りません。

*3:GroovyServも同じ問題を抱えているので、真似すると良いかも。