Compile Scala Code At Runtime

The following is some copy&paste from the Eval class from twitter utils to extract a minified version that compiles source strings.

The basic idea is this: The compiler compiles code into class files that are then written to some directory. The compiler holds a class loader that is serving class files from this directory. So after some class has been successfully compiled, it is available via this class loader. The directory of class files doesn't need to be a real one on the file system, it can be a virtual directory in memory.

Since only string snippets are compiled, they are wrapped into some class which can be compiled. The class name is created by taking the SHA checksum of the snippet.

import scala.tools.nsc.{Global, Settings}
import tools.nsc.util.BatchSourceFile
import tools.nsc.io.{VirtualDirectory, AbstractFile}
import tools.nsc.interpreter.AbstractFileClassLoader
import java.security.MessageDigest
import java.math.BigInteger
import collection.mutable
import java.io.File

object CompileTest {
  val compiler = new Compiler(None)

  def main(args: Array[String]) {
    val script = "println(List(1, 2, 3, 4).mkString(\" + \"))"
    compiler.eval[Unit](script)
  }
}

class Compiler(targetDir: Option[File]) {

  val target = targetDir match {
    case Some(dir) => AbstractFile.getDirectory(dir)
    case None => new VirtualDirectory("(memory)", None)
  }

  val classCache = mutable.Map[String, Class[_]]()

  private val settings = new Settings()
  settings.deprecation.value = true // enable detailed deprecation warnings
  settings.unchecked.value = true // enable detailed unchecked warnings
  settings.outputDirs.setSingleOutput(target)
  settings.usejavacp.value = true

  private val global = new Global(settings)
  private lazy val run = new global.Run

  val classLoader = new AbstractFileClassLoader(target, this.getClass.getClassLoader)

  /**Compiles the code as a class into the class loader of this compiler.
   *
   * @param code
   * @return
   */
  def compile(code: String) = {
    val className = classNameForCode(code)
    findClass(className).getOrElse {
      val sourceFiles = List(new BatchSourceFile("(inline)", wrapCodeInClass(className, code)))
      run.compileSources(sourceFiles)
      findClass(className).get
    }
  }

  /** Compiles the source string into the class loader and
   * evaluates it.
   *
   * @param code
   * @tparam T
   * @return
   */
  def eval[T](code: String): T = {
    val cls = compile(code)
    cls.getConstructor().newInstance().asInstanceOf[() => Any].apply().asInstanceOf[T]
  }

  def findClass(className: String): Option[Class[_]] = {
    synchronized {
      classCache.get(className).orElse {
        try {
          val cls = classLoader.loadClass(className)
          classCache(className) = cls
          Some(cls)
        } catch {
          case e: ClassNotFoundException => None
        }
      }
    }
  }

  protected def classNameForCode(code: String): String = {
    val digest = MessageDigest.getInstance("SHA-1").digest(code.getBytes)
    "sha"+new BigInteger(1, digest).toString(16)
  }

  /*
  * Wrap source code in a new class with an apply method.
  */
  private def wrapCodeInClass(className: String, code: String) = {
    "class " + className + " extends (() => Any) {\n" +
      "  def apply() = {\n" +
      code + "\n" +
      "  }\n" +
      "}\n"
  }
}

Or this test

def compile(args: String*) = {
  val settings = newSettings((CommandLineParser tokenize extraSettings) ++
     args.toList)
  val global   = new Global(settings)
  new global.Run compileSources List(new BatchSourceFile("<partest>", code))
  !global.reporter.hasErrors
}

Date: [2013-11-26 Di]

Created: 2015-05-25 Mo 21:35

Validate