Создание сериализуемых объектов из исходного кода Scala во время выполнения


Чтобы внедрить Scala в качестве "языка сценариев", мне нужно уметь компилировать текстовые фрагменты в простые объекты, такие как Function0[Unit], которые могут быть сериализованы и десериализованы с диска и которые могут быть загружены в текущую среду выполнения и выполнены.

Как бы я это сделал?

Скажем, например, мой текстовый фрагмент (чисто гипотетический):
Document.current.elements.headOption.foreach(_.open())

Это может быть обернуто в следующий полный текст:

package myapp.userscripts
import myapp.DSL._

object UserFunction1234 extends Function0[Unit] {
  def apply(): Unit = {
    Document.current.elements.headOption.foreach(_.open())
  }
}

Что будет дальше? Должен ли я использовать IMain для компиляции этого код? Я не хочу использовать обычный режим интерпретатора, потому что компиляция должна быть "контекстно-свободной" и не накапливать запросы.

Что мне нужно, чтобы получить отсрочку от компиляции, это, наверное, двоичный файл класса? В этом случае сериализация является прямой (массив байтов). Как бы я тогда загрузил этот класс в среду выполнения и вызвал метод apply?

Что происходит, если код компилируется в несколько вспомогательных классов? Приведенный выше пример содержит замыкание _.open(). Как мне это сделать убедитесь, что я" упаковываю " все эти вспомогательные вещи в один объект для сериализации и загрузки классов?


Примечание : учитывая, что Scala 2.11 неизбежна и API компилятора, вероятно, изменился, я рад получить подсказки о том, как подойти к этой проблеме на Scala 2.11

1 4

1 ответ:

Вот одна идея: использовать обычный экземпляр компилятора Scala. К сожалению, это, кажется, требует использования файлов жесткого диска как для ввода, так и для вывода. Поэтому мы используем для этого временные файлы. Выходные данные будут заархивированы в JAR, который будет храниться в виде массива байтов (что войдет в гипотетический процесс сериализации). Нам нужен специальный загрузчик классов, чтобы снова извлечь класс из извлеченной банки.

Следующее предполагает Scala 2.10.3 с библиотекой scala-compiler в классе путь:

import scala.tools.nsc
import java.io._
import scala.annotation.tailrec

Перенос кода, предоставленного Пользователем, в класс функций с синтетическим именем, которое будет увеличиваться для каждого нового фрагмента:

val packageName = "myapp"

var userCount = 0

def mkFunName(): String = {
  val c = userCount
  userCount += 1
  s"Fun$c"
}

def wrapSource(source: String): (String, String) = {
  val fun = mkFunName()
  val code = s"""package $packageName
                |
                |class $fun extends Function0[Unit] {
                |  def apply(): Unit = {
                |    $source
                |  }
                |}
                |""".stripMargin
  (fun, code)
}

Функция для компиляции исходного фрагмента и возврата массива байтов результирующего jar:

/** Compiles a source code consisting of a body which is wrapped in a `Function0`
  * apply method, and returns the function's class name (without package) and the
  * raw jar file produced in the compilation.
  */
def compile(source: String): (String, Array[Byte]) = {
  val set             = new nsc.Settings
  val d               = File.createTempFile("temp", ".out")
  d.delete(); d.mkdir()
  set.d.value         = d.getPath
  set.usejavacp.value = true
  val compiler        = new nsc.Global(set)
  val f               = File.createTempFile("temp", ".scala")
  val out             = new BufferedOutputStream(new FileOutputStream(f))
  val (fun, code)     = wrapSource(source)
  out.write(code.getBytes("UTF-8"))
  out.flush(); out.close()
  val run             = new compiler.Run()
  run.compile(List(f.getPath))
  f.delete()

  val bytes = packJar(d)
  deleteDir(d)

  (fun, bytes)
}

def deleteDir(base: File): Unit = {
  base.listFiles().foreach { f =>
    if (f.isFile) f.delete()
    else deleteDir(f)
  }
  base.delete()
}

Примечание : пока не обрабатывает ошибки компилятора!

Метод packJar использует выходной каталог компилятора и создает из него файл jar в памяти:

// cf. http://stackoverflow.com/questions/1281229
def packJar(base: File): Array[Byte] = {
  import java.util.jar._

  val mf = new Manifest
  mf.getMainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0")
  val bs    = new java.io.ByteArrayOutputStream
  val out   = new JarOutputStream(bs, mf)

  def add(prefix: String, f: File): Unit = {
    val name0 = prefix + f.getName
    val name  = if (f.isDirectory) name0 + "/" else name0
    val entry = new JarEntry(name)
    entry.setTime(f.lastModified())
    out.putNextEntry(entry)
    if (f.isFile) {
      val in = new BufferedInputStream(new FileInputStream(f))
      try {
        val buf = new Array[Byte](1024)
        @tailrec def loop(): Unit = {
          val count = in.read(buf)
          if (count >= 0) {
            out.write(buf, 0, count)
            loop()
          }
        }
        loop()
      } finally {
        in.close()
      }
    }
    out.closeEntry()
    if (f.isDirectory) f.listFiles.foreach(add(name, _))
  }

  base.listFiles().foreach(add("", _))
  out.close()
  bs.toByteArray
}

Служебная функция, которая принимает массив байтов найден в десериализации и создает карту из имен классов в байт-код класса:

def unpackJar(bytes: Array[Byte]): Map[String, Array[Byte]] = {
  import java.util.jar._
  import scala.annotation.tailrec

  val in = new JarInputStream(new ByteArrayInputStream(bytes))
  val b  = Map.newBuilder[String, Array[Byte]]

  @tailrec def loop(): Unit = {
    val entry = in.getNextJarEntry
    if (entry != null) {
      if (!entry.isDirectory) {
        val name  = entry.getName  
        // cf. http://stackoverflow.com/questions/8909743
        val bs  = new ByteArrayOutputStream
        var i   = 0
        while (i >= 0) {
          i = in.read()
          if (i >= 0) bs.write(i)
        }
        val bytes = bs.toByteArray
        b += mkClassName(name) -> bytes
      }
      loop()
    }
  }
  loop()
  in.close()
  b.result()
}

def mkClassName(path: String): String = {
  require(path.endsWith(".class"))
  path.substring(0, path.length - 6).replace("/", ".")
}

Подходящий загрузчик класса:

class MemoryClassLoader(map: Map[String, Array[Byte]]) extends ClassLoader {
  override protected def findClass(name: String): Class[_] =
    map.get(name).map { bytes =>
      println(s"defineClass($name, ...)")
      defineClass(name, bytes, 0, bytes.length)

    } .getOrElse(super.findClass(name)) // throws exception
}

И тестовый случай, содержащий дополнительные классы (замыкания):

val exampleSource =
  """val xs = List("hello", "world")
    |println(xs.map(_.capitalize).mkString(" "))
    |""".stripMargin

def test(fun: String, cl: ClassLoader): Unit = {
  val clName  = s"$packageName.$fun"
  println(s"Resolving class '$clName'...")
  val clazz = Class.forName(clName, true, cl)
  println("Instantiating...")
  val x     = clazz.newInstance().asInstanceOf[() => Unit]
  println("Invoking 'apply':")
  x()
}

locally {
  println("Compiling...")
  val (fun, bytes) = compile(exampleSource)

  val map = unpackJar(bytes)
  println("Classes found:")
  map.keys.foreach(k => println(s"  '$k'"))

  val cl = new MemoryClassLoader(map)
  test(fun, cl)   // should call `defineClass`
  test(fun, cl)   // should find cached class
}