Создание сериализуемых объектов из исходного кода 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 ответ

Решение

Вот одна идея: использовать обычный экземпляр компилятора Scala. К сожалению, это требует использования файлов жесткого диска как для ввода, так и для вывода. Поэтому мы используем временные файлы для этого. Вывод будет заархивирован в JAR, который будет сохранен как байтовый массив (который войдет в гипотетический процесс сериализации). Нам нужен специальный загрузчик классов, чтобы снова извлечь класс из извлеченного 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://stackru.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://stackru.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
}
Другие вопросы по тегам