11

I have the following code that works in Scala 2.10 to compile external classes at runtime in Scala

/**
  * Compile scala files and keep them loaded in memory
  * @param classDir Directory storing the generated scala files
  * @throws IOException if there is problem reading the source files
  * @return Classloader that contains the compiled external classes
  */
@throws[IOException]
def compileFiles(classDir: String): AbstractFileClassLoader = {
  val files = recursiveListFiles(new File(classDir))
                  .filter(_.getName.endsWith("scala"))
  println("Loaded files: \n" + files.mkString("[", ",\n", "]"))

  val settings: GenericRunnerSettings = new GenericRunnerSettings(err => println("Interpretor error: " + err))
  settings.usejavacp.value = true
  val interpreter: IMain = new IMain(settings)
  files.foreach(f => {
    interpreter.compileSources(new BatchSourceFile(AbstractFile.getFile(f)))
  })

  interpreter.getInterpreterClassLoader()
}

And then elsewhere, I could use the classloader reference to instantiate classes e.g.

val personClass = classLoader.findClass("com.example.dynacsv.PersonData")
val ctor = personClass.getDeclaredConstructors()(0)
val instance = ctor.newInstance("Mr", "John", "Doe", 25: java.lang.Integer, 165: java.lang.Integer, 1: java.lang.Integer)
println("Instantiated class: " + instance.getClass.getCanonicalName)
println(instance.toString)

However the above no longer works as getInterpreterClassLoader method has been removed from scala.tools.nsc.interpreter.IMain. Also, AbstractFileClassLoader has been moved and deprecated. It is no longer allowed to call findClass method in the class loader from an external package.

What is the recommended way to do the above in Scala 2.11? Thanks!

vsnyc
  • 1,997
  • 20
  • 30

1 Answers1

19

If your goal is to run external scala classes in runtime, I'd suggest using eval with scala.tools.reflect.ToolBox (it is included in REPL, but for normal usage you have to add scala-reflect.jar):

import scala.reflect.runtime.universe
import scala.tools.reflect.ToolBox
val tb = universe.runtimeMirror(getClass.getClassLoader).mkToolBox()
tb.eval(tb.parse("""println("hello!")"""))

You also can compile files, using tb.compile.

Modified with example: assume you have external file with

class PersonData() {
  val field = 42
}
scala.reflect.classTag[PersonData].runtimeClass

So you do

val clazz = tb.compile(tb.parse(src))().asInstanceOf[Class[_]]
val ctor = clazz.getDeclaredConstructors()(0)
val instance = ctor.newInstance()

Additional possibilities are (almost) unlimited, you can get full tree AST and work with it as you want:

showRaw(tb.parse(src)) // this is AST of external file sources
// this is quasiquote
val q"""
      class $name {
        ..$stats
      }
      scala.reflect.classTag[PersonData].runtimeClass
    """ = tb.parse(src)
// name: reflect.runtime.universe.TypeName = PersonData
// stats: List[reflect.runtime.universe.Tree] = List(val field = 42)
println(name) // PersonData

See official documentation for these tricks:

http://docs.scala-lang.org/overviews/reflection/symbols-trees-types.html

http://docs.scala-lang.org/overviews/quasiquotes/intro.html

dveim
  • 2,981
  • 2
  • 20
  • 28
  • How do I get back instance of an unknown class after doing the compile? `tb.compile(tb.parse(source)).asInstanceOf[PersonData]` does not work, because PersonData is in the source being compiled. There is a working example added in the [linked question](http://stackoverflow.com/questions/39088871/dynamically-create-case-class?noredirect=1&lq=1), would you be able to modify that to use `ToolBox`? Thanks – vsnyc Aug 25 '16 at 14:43
  • Can you modify sources to have reference to defined class in final statement? If yes, then all you need is to grab that class with `asInstanceOf[Class[_]]`, and use it as you want. – dveim Aug 25 '16 at 15:01
  • 1
    I mean having in source file smth like `case class PersonData(i: Int); scala.reflect.classTag[PersonData].runtimeClass` – dveim Aug 25 '16 at 15:03
  • Modified my answer with an example. Note that you have to continue using reflection to access fields. – dveim Aug 25 '16 at 15:19
  • 1
    Thanks, yes I can modify the source file and add the classTag line. With that this works now. One question: `instance.getClass.getName` gives something like: `__wrapper$1$3ec23e8228d44c638bba98e65ef2c748.__wrapper$1$3ec23e8228d44c638bba98e65ef2c748$PersonData$3`. Is there any way to get the `PersonData` as the class name?. Also, you have a typo (missed the apply) in your code for `val clazz`, please fix it so it helps someone else. It should be `val clazz = tb.compile(tb.parse(src))().asInstanceOf[Class[_]]` – vsnyc Aug 25 '16 at 16:27
  • Also, is there a way to specify a package name for the class? If I add `package com.example.csv` at the top of external file, it errors out. I tried with or without wrapping the package in `{ ... }` – vsnyc Aug 25 '16 at 16:48
  • Thanks, fixed that typo. Also added answer for first question. What about second one, I'm confused what is going on, need some research for that weird error. Can you simply ignore that package line while constructing `src` (a workaround) ? – dveim Aug 25 '16 at 17:59
  • Great, I'll leave the question open for a couple of days to see if there are any better ideas to do this, but you're answer and comments have helped me. Thanks! – vsnyc Aug 25 '16 at 18:03
  • I get an AbstractMethodError when calling `tb.eval`. I am executing the code sample with the `println("hello!")` in it. I have added `scala-reflect.jar` as a dependancy. I am using Scala 1.12. Any ideas? – Llew Vallis Dec 14 '17 at 06:58
  • @LlewVallis (probably you meant `2.12`). Just tried, works for me (run `App` with `scala-reflect.jar`, first code snippet works). Please check versions of your dependencies. Then, if that doesn't help, could you publish your code somewhere, so I could take a look on it? – dveim Dec 14 '17 at 11:45
  • @vsnyc @dveim Thanks for the detailed description. Did you guys ever found out the solution of getting `PersonData` as className instead of `__wrapper$1$3ec23e8228d44c638bba98e65ef2c748.__wrapper$1$3ec23e8228d44c638bba98e65ef2c748$PersonData$3` – Sumeet Gupta Sep 13 '18 at 06:46
  • @SumeetGupta I wasn't able to get the package name added at the time. – vsnyc Sep 18 '18 at 21:26
  • Hi, do you know by chance which is the maven depedency for import scala.tools.reflect.ToolBox? – Ignacio Alorre Oct 16 '19 at 09:07
  • @IgnacioAlorre https://mvnrepository.com/artifact/org.scala-lang/scala-compiler . Though "scala-reflect" library alone should be sufficient. – dveim Oct 16 '19 at 09:29