S-JIS[2011-03-36/2011-09-12] 変更履歴
Scalaのコンパイラプラグインのメモ。
|
コンパイラプラグインは、Scalaコンパイラー(scalac)にロジックを追加し、コンパイル時に独自の処理(ソースをチェックしてエラーを出したりコードを置換したり)を行えるようにするもの。
Scalacではフェーズと呼ばれる段階があって、それぞれのフェーズで個別のコンパイル処理を行っている。
自作プラグインは、新しいフェーズを作ってコンパイル時に呼ばれるようにして独自処理を行う。
Pluginクラスを継承し、コンストラクターでGlobalを受け取るようにする。
そして、プラグイン名などの情報を返すメソッドをオーバーライドする。
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ファイル内のクラスパスの一番上(デフォルトパッケージの位置)に配置する)
<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ファイル化する。
@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 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でのScalaソースをコンパイルする際に自作コンパイラプラグインを実行させることも出来る。[2011-09-10]
ちなみに、ソースの一部分だけを修正してコンパイルされる際はコンパイラプラグインは呼ばれないっぽい。
プラグインでソース解析を行うのに使用するクラス。
クラス名 | 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] |