S-JIS[2011-03-36/2011-09-12] 変更履歴

Scala コンパイラプラグイン

Scalaのコンパイラプラグインのメモ。


概要

コンパイラプラグインは、Scalaコンパイラー(scalac)にロジックを追加し、コンパイル時に独自の処理(ソースをチェックしてエラーを出したりコードを置換したり)を行えるようにするもの。

Scalacではフェーズと呼ばれる段階があって、それぞれのフェーズで個別のコンパイル処理を行っている。
自作プラグインは、新しいフェーズを作ってコンパイル時に呼ばれるようにして独自処理を行う。


最小限の動作確認サンプル


プラグインのソース

Pluginクラスを継承し、コンストラクターでGlobalを受け取るようにする。
そして、プラグイン名などの情報を返すメソッドをオーバーライドする。

cplugin.scala

package jp.hishidama.scalac.plugin
import scala.tools.nsc.{ Global, Phase }
import scala.tools.nsc.plugins.{ Plugin, PluginComponent }
class SamplePlugin(val global: Global) extends Plugin {
  selfPlugin =>

  import global._

  override val name = "plugin-sample"                          //プラグインの名前
  override val description = "scala compiler plugin sample"    //プラグインの説明。-Xplugin-listで見られる
  override val components = List[PluginComponent](Component) //プラグイン本体を返す

componentsで、自作プラグイン(コンポーネント)を返すようにする。

  private object Component extends PluginComponent {

    override val global: selfPlugin.global.type = selfPlugin.global
    override val runsAfter  = List("refchecks")                //どのフェーズの後に実行するかを指定する
    override val runsBefore = List("")                         //どのフェーズの前に実行するかを指定する
    override val phaseName = selfPlugin.name                   //フェーズ名。-Xshow-phasesで見られる

    override def newPhase(prev: Phase) = new StdPhase(prev) {
      override def name = selfPlugin.name
      override def apply(unit: CompilationUnit): Unit = {
        printf("unit.body=%s%n%s%n", unit.body.getClass, unit.body)
      }
    }
  }
}

newPhase()メソッドで、新しいフェーズ(コンパイルのフェーズ)を作成する。
プラグインが実行されると自作フェーズのapply()メソッドが呼ばれるので、その中でソースを解析して独自処理を行う。


プラグインの設定ファイル

プラグインの設定(クラス)をscalac-plugin.xmlというファイルに書く必要がある。
(このファイルは、jarファイル内のクラスパスの一番上(デフォルトパッケージの位置)に配置する)

scalac-plugin.xml

<plugin>
  <name>plugin-sample</name>
  <classname>jp.hishidama.scalac.plugin.SamplePlugin</classname>
</plugin>

このname要素の内容は、プラグインのプログラム内で指定しているプラグイン名と一致している必要は無いようだ。


プラグイン自体のコンパイル

プラグインを実行するときはjarファイルになっている必要があるので、jarファイル化する。
そのjarファイル内のルート(一番トップ)にscalac-plugin.xmlが無ければならない。

下記のバッチでは、「classes」ディレクトリーを作成し、そこにプラグインのソースをコンパイルする。
そしてカレントディレクトリーにあるscalac-plugin.xmlをclasses直下にコピーし、classes配下をjarファイル化する。

compile.bat:

@mkdir classes 2> nul
call scalac -d classes cplugin.scala
@echo on
copy scalac-plugin.xml classes\
jar cf cplugin.jar -C classes .

Windowsのscalacはバッチファイル(scalac.bat)なので、自分のバッチファイルから呼び出すときはcallを使う必要がある。
また、scalac.batは内部でecho offを呼んでいるようなので、戻すには自分でecho onをする必要がある。


プラグインの実行

プラグインを実行するには、scalacに-Xpluginでプラグインのjarファイルを指定する。

プラグイン一覧の表示

> scalac -Xplugin:cplugin.jar -Xplugin-list
plugin-sample - scala compiler plugin sample
continuations - applies selective cps conversion

フェーズ一覧の表示

> scalac -Xplugin:cplugin.jar -Xshow-phases
    phase name  id  description
    ----------  --  -----------
        parser   1  parse source into ASTs, perform simple desugaring
         namer   2  resolve names, attach symbols to named trees
〜
     refchecks   7  reference/override checking, translate nested objects
  selectiveanf   8
      liftcode   9  reify trees
 plugin-sample  10
  selectivecps  11
〜
           jvm  27  generate JVM bytecode
      terminal  28  The last phase in the compiler chain

※Scala2.8まではフェーズ名しか表示されなかったが、Scala2.9以降はidと説明も表示されるようになった。[2011-09-02]

プラグインの実行

> scalac -Xplugin:cplugin.jar hello.scala
unit.body=class scala.reflect.generic.Trees$PackageDef
package <empty> {
  final class Hello extends java.lang.Object with ScalaObject {
    def this(): object Hello = {
      Hello.super.this();
      ()
    };
    def main(args: Array[String]): Unit = scala.this.Predef.println("hello")
  }
}

また、プラグインのjarファイルを「%SCALA_HOME%\misc\scala-devel\plugins」の下に置いておけば、-Xpluginを指定しなくてもいいらしい。

ちなみに、ここでコンパイルしたhello.scalaはhelloを表示する(何の変哲も無い)プログラム。

object Hello {
  def main(args: Array[String]): Unit = {
    println("hello")
  }
}

デバッグ実行

コンパイラプラグインのデバッグ方法について。[2011-09-10]
コンパイラプラグインを実行していると、SymbolやTreeが思った状態にならなくてエラーが頻発する(苦笑)ので、デバッグでステップ実行しながら変数の内容を確認したくなる。

scalacの実体はjavaプログラムなので、Javaのリモートデバッグ機能を利用してEclipseでデバッグ実行することが出来る。
Windowsのscalac.batではJavaVMのオプションを渡せないが、環境変数JAVA_OPTSでオプションを指定できる。

> set JAVA_OPTS= -agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=y
> scalac -Xplugin:cplugin.jar hello.scala

ただし、通常のScala実行用のscala.batやscalapもJAVA_OPTSの影響を受けてしまうので注意。


Eclipseから直接プラグイン(コンパイル)を実行する方法もある。
参考: Develop using Eclipse

jarファイルの中にscalac-plugin.xmlだけ格納しておき、クラス群(作成したプラグイン本体)はEclipseのプロジェクトのclassesをそのまま参照する。
(Eclipseでコンパイラプラグインを開発する場合、ビルドパスにSCALA_HOME/lib/scala-compiler.jarを追加しておく)

xmlだけjarファイル化するbuild.xml(プロジェクト/bin/build.xml):

<?xml version="1.0" encoding="UTF-8"?>
<project name="scalac-plugin" basedir=".">

    <target name="xmlonly.jar">
        <jar jarfile="xmlonly.jar">
            <fileset dir="../classes" includes="**/*.xml" />
        </jar>
    </target>
</project>

そして、「実行の構成」を構築しscala.tools.nsc.Mainを実行する。

タブ 項目 設定例 備考
Main Project scalac-plugin コンパイラプラグインを開発しているプロジェクト。
Main class scala.tools.nsc.Main コンパイル実行クラス。
Argument Program arguments -Xplugin:bin/xmlonly.jar C:/temp/hello.scala scalacに渡すオプションそのもの。

この方法なら、デバッグ実行も簡単にできる。


Eclipseでの実行

EclipseでのScalaソースをコンパイルする際に自作コンパイラプラグインを実行させることも出来る。[2011-09-10]

  1. コンパイラプラグインを実行させる対象のプロジェクトのプロパティーを開く。
  2. 左側のツリーから「Scala Compiler」を選択する。
  3. 「Use Project Settings」にチェックを入れる。
  4. 「Advanced」タブを選択する。
  5. 「Xplugin」にプラグインのjarファイルを指定する。

ちなみに、ソースの一部分だけを修正してコンパイルされる際はコンパイラプラグインは呼ばれないっぽい。


プラグイン関連のクラス

プラグインでソース解析を行うのに使用するクラス。

クラス名 Scaladoc ソース(Scala2.8) 備考
scala.tools.nsc   package nsc  
scala.tools.nsc.plugins Plugin Plugin Plugin.scala  
scala.tools.nsc Global Global Global.scala  
scala.tools.nsc global.CompilationUnit CompilationUnit CompilationUnits.scala GlobalはCompilationUnitsトレイトをミックスインしており、
その内部でCompilationUnitクラスが定義されている。
scala.reflect.generic Trees#Tree Tree Trees.scala PackageDefやDefDef・ValDef、ApplyやSelect等の
Treeの派生クラスも定義されている。
Treeの生成方法
scala.tools.nsc.ast Trees#Traverser Traverser ast/Trees.scala  
scala.tools.nsc.transform Transform
InfoTransform
TypingTransformers
    ソースコード変換に使用するトレイト。[2011-09-08]

Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま