CLOVER🍀

That was when it all began.

動的コンパイル結果から、Scalaのobjectを触る

先週、ScalaのCompiler APIを使ってclassの定義を実行時にコンパイルしたり、スクリプトを走らせたりしました。

今回は、Scalaのobject構文で宣言されたオブジェクトを扱ってみたいと思います。
ちょっと癖がありましたので。

まずは、今回のスケープゴート。
HelloWorldObject.scala

object HelloWorldObject {
  println("Hello World Object")
  def foo(): Unit = {
    println("Hello Object foo")
  }
}

なんの変哲もない、ただのScala Objectです。メソッドはfooのみ宣言してあります。まずは、これを普通にコンパイルしてjavapしてみます。

$ fsc HelloWorldObject.scala 
$ javap HelloWorldObject\$
Compiled from "HelloWorldObject.scala"
public final class HelloWorldObject$ extends java.lang.Object implements scala.ScalaObject{
    public static final HelloWorldObject$ MODULE$;
    public static {};
    public void foo();
}

結果を見ればわかるように、メソッドfooはstaticメソッドではありません。さらに、クラス名は「HelloWorldObject」ではなく、「HelloWorldObject$」であることがポイントです。

これを踏まえると、このクラス…というかオブジェクトを動的にコンパイルして操作するためのコードは、先週のサンプルを引用して考えると、以下のようになります。

object CompileObjectSourceRunner {
  def main(args: Array[String]): Unit = {
    args.headOption match {
      case None =>
        println("""Required One or more Arguments
                   |  Usage: [SourcePath]:[ClassName]*""".stripMargin)
        sys.exit(1)
      case _ =>
        val compiler = new DynamicCompiler
	args foreach { pair =>
          val (path, className) = (pair.split(":")(0), pair.split(":")(1))
          compiler.compileClassFromFile(path, className + "$") match {
              case Some(c) =>
                println("Compile Success[%s]".format(c.getName))
                try {
                  val module = c.getDeclaredField("MODULE$")
                  c.getDeclaredMethod("foo").invoke(module.get(null))
                } catch {
                  case e => e.printStackTrace()
                }
              case None => println("Compile Fail")
          }
        }
    }
  }
}

ポイントは、ここと

          compiler.compileClassFromFile(path, className + "$") match {

ここ。

                  val module = c.getDeclaredField("MODULE$")
                  c.getDeclaredMethod("foo").invoke(module.get(null))

クラス名を指定するのに、「$」を入れさせるのはあんまりな感じがしたので、解釈側で補うようにしています。そして、フィールドMODULE$を取得して、これ経由でメソッドを呼び出すことになります。

実行結果は、以下の通り。

> run-main CompileObjectSourceRunner ../HelloWorldObject.scala:HelloWorldObject
[info] Compiling 1 Scala source to /xxxxx/dynamic-compiler/target/scala-2.9.0.final/classes...
[info] Running CompileObjectSourceRunner ../HelloWorldObject.scala:HelloWorldObject
Compile Success[HelloWorldObject$]
Hello World Object
Hello Object foo
[success] Total time: 34 s, completed Aug 14, 2011 5:13:55 PM

汎用クラスのようで、今回の起動プログラムは引数無しのfooメソッドを持ったクラスにしか適用できないのですが、まあサンプルということで…。