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