Создание сериализуемых объектов из исходного кода 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, который будет храниться в виде массива байтов (что войдет в гипотетический процесс сериализации). Нам нужен специальный загрузчик классов, чтобы снова извлечь класс из извлеченной банки.
Следующее предполагает 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 }