Как стать автором
Обновить
337.6
TINKOFF
IT’s Tinkoff — просто о сложном

Простая компиляция Scala-кода во время исполнения

Время на прочтение9 мин
Количество просмотров8.8K
Итак, приступим пожалуй. Я люблю Scala не только за то, что она позволяет писать в два раза меньше кода, чем на Java. Понятного и выразительного кода. Помимо этого на Scala можно делать вещи вообще недоступные разработчикам на Java: generic’и высшего порядка, узнавать типы generic’ов в runtime при помощи манифестов.

Одна из таких вещей, о которой пойдёт речь — это компиляция Scala-кода во время исполнения программы. Это может быть нужно, когда хочется выполнить приходящий из удалённого источника код или написать самомодифицируемую программу, аналог функции eval в JS.

Важнейшее отличие Scala от Java в том, что её компилятор — не native программа, как javac, а поставляется в виде jar-файла, то есть является простой библиотекой, которую никто не мешает вызвать когда угодно, чтобы из исходных кодов получить байткод JVM.

Единственный недостаток этого метода — Scala Compiler API имеет не очень удобный интерфейс для простых задач: когда нужно просто взять исходник и получить из него пачку скомпилированных классов, по которым уже через Java Reflection API можно будет создавать экземпляры. Именно поэтому я решился однажды создать для компиляции простую обёртку, о которой и будет рассказ. Код для неё я собирал по крупицам со Stack Overflow и тематических сайтов.

Интерфейс для компиляции кода Scala предоставляется классом scala.tools.nsc.Global. Для вызова компиляции необходимо создать экземпляр этого класса, потом экземпляр вложенного класса Run и запустить метод compileSources:

val compiler = new Global(settings, reporter)<br/>
new compiler.Run()compileSources(sources)

Тут всё достаточно просто, но для компиляции нужны настройки (settings), reporter, который будет собирать ошибки компиляции и собственно исходники (sources).

Настройки имеют тип scala.tools.nsc.Settings, в настройках для моих нужд хватило двух параметров: usejavacp, определяющего, что компилятору надо использовать текущий classpath Java (это полезно, поскольку исходные коды, которые вы хотите скомпилировать могут содержать ссылки на внешние классы из окружения), и второго параметра outputDirs. Дело в том, что компилятор, который находится в классе Global, умеет складывать скомпилированные классы только в виде файлов на диске. Мне не хотелось привязываться к диску, так что я нашёл обходное решение, достаточно популярное, кстати — использовать виртуальную директорию в памяти.

В итоге мой код для настроек выглядит так:

val settings = new Settings<br/>
 <br/>
settings.usejavacp.value = true<br/>
 <br/>
val directory = new VirtualDirectory("(memory)", None)<br/>
settings.outputDirs.setSingleOutput(directory)

Дальше надо настроить reporter. Я использовал scala.tools.nsc.reporters.StoreReporter:

val reporter = new StoreReporter()

Он сохраняет все проблемы с компиляцией внутри себя, и ошибки можно будет проанализировать так уже после того, как компиляция закончится.

И последнее, что надо сделать — подготовить исходники. Всё не так просто и компилятор принимает список из экземпляров scala.tools.nsc.util.SourceFile, в то время как у нас, как вы помните, есть только строчка с исходным кодом. Спасибо короткому синтаксису Scala, преобразование делается просто:

val sources = List(new BatchSourceFile("<source>", source))

Мы создаёт один файл, куда помещаем исходный код.

Теперь у нас всё готово, мы можем вызвать компилятор и первое, что надо сделать — проверить: а как же скомпилировались наши исходники:

if (reporter.hasErrors) {<br/>
  throw new CompilationFailedException(source,<br/>
    reporter.infos.map(info => (info.pos.line, info.msg)))<br/>
}

Для простоты, в случае наличия ошибок (а не предупреждений) я кидаю исключение о том, что компиляция неуспешна, вот это исключение:

class CompilationFailedException(val programme: String, val messages: Iterable[(Int, String)])<br/>
  extends Exception(messages.map(message => message._1 + ". " + message._2).mkString("n"))

Если всё пошло хорошо, значит мы уже имеет собранные классы, осталось их как-то загрузить из той самой виртуальной папки. Используем встроенный класс scala.tools.nsc.interpreter.AbstractFileClassLoader:

val classLoader =  new AbstractFileClassLoader(directory, this.getClass.getClassLoader())

AbstractFileClassLoader будет создаваться новый при каждой новой компиляции, чтобы, если мы захотим перекомпилировать какой-нибудь класс, он успешно загрузился во второй раз, не конфликтуя с предыдущей своей инкарнацией.

После создания class loader’а, нужно пройти по файлам виртуальной папки и подгрузить классы из файлов внутри неё. В Scala один файл с исходниками может содержать несколько классов, не обязательно вложенных внутрь друг друга, при компиляции в байткод JVM такие несколько классов будут разложены в несколько файлов, причём вложенные классы будут лежать в отдельных классах с именем ClassName$InnerClassName.class. Я использовал этот код для компиляции реализаций одного интерфейса, так, что я всегда знал чего ожидать от полученных классов. Кстати поэтому вложенные классы, которые норовили встать в один ряд с основными мне сильно мешались, при загрузке я их пропускаю при наличии в названии знака $:

for (classFile <- directory; if (!classFile.name.contains('$'))) yield {<br/>
 <br/>
  val path = classFile.path<br/>
  val fullQualifiedName = path.substring(path.indexOf('/')+1, path.lastIndexOf('.')).replace("/"".")<br/>
 <br/>
  classLoader.loadClass(fullQualifiedName)<br/>
}

Для загрузки классов нужно получать их полные названия (fully qualified name), включающие структуру пакетов. Для воссоздания этого названия я использовал манипуляцию с путём в папке.

Ну что же, теперь наши классы загружены и конструкция выше вернёт список этих классов.

На этом всё. Вот так простая вроде бы, хотя и нетривиальная задача, смогла быть выполнена только написанием страницы кода.

Полный текст получившегося класса:

/*!# Compiler<br/>
 <br/>
  This class is a wrapper over Scala Compiler API<br/>
  which has simple interface just accepting the source code string.<br/>
 <br/>
  Compiles the source code assuming that it is a .scala source file content.<br/>
  It used a classpath of the environment that called the `Compiler` class.<br/>
 */
<br/>
 <br/>
import tools.nsc.{Global, Settings}<br/>
import tools.nsc.io._<br/>
import tools.nsc.reporters.StoreReporter<br/>
import tools.nsc.interpreter.AbstractFileClassLoader<br/>
import tools.nsc.util._<br/>
 <br/>
class Compiler {<br/>
 <br/>
  def compile(source: String)Iterable[Class[_]] = {<br/>
 <br/>
    // prepare the code you want to compile<br/>
    val sources = List(new BatchSourceFile("<source>", source))<br/>
 <br/>
    // Setting the compiler settings<br/>
    val settings = new Settings<br/>
 <br/>
    /*! Take classpath from currently running scala environment. */<br/>
    settings.usejavacp.value = true<br/>
 <br/>
    /*! Save class files for compiled classes into a virtual directory in memory. */<br/>
    val directory = new VirtualDirectory("(memory)", None)<br/>
    settings.outputDirs.setSingleOutput(directory)<br/>
 <br/>
    val reporter = new StoreReporter()<br/>
    val compiler = new Global(settings, reporter)<br/>
    new compiler.Run()compileSources(sources)<br/>
 <br/>
    /*! After the compilation if errors occured, `CompilationFailedException`<br/>
        is being thrown with a detailed message. */
<br/>
    if (reporter.hasErrors) {<br/>
      throw new CompilationFailedException(source,<br/>
        reporter.infos.map(info => (info.pos.line, info.msg)))<br/>
    }<br/>
 <br/>
    /*! Each time new `AbstractFileClassLoader` is created for loading classes<br/>
      it gives an opportunity to treat same name classes loading well.<br/>
     */
<br/>
    // Loading new compiled classes<br/>
    val classLoader =  new AbstractFileClassLoader(directory, this.getClass.getClassLoader())<br/>
 <br/>
    /*! When classes are loading inner classes are being skipped. */<br/>
    for (classFile <- directory; if (!classFile.name.contains('$'))) yield {<br/>
 <br/>
      /*! Each file name is being constructed from a path in the virtual directory. */<br/>
      val path = classFile.path<br/>
      val fullQualifiedName = path.substring(path.indexOf('/')+1,path.lastIndexOf('.')).replace("/",".")<br/>
 <br/>
      Console.println(fullQualifiedName)<br/>
 <br/>
      /*! Loaded classes are collecting into a returning collection with `yield`. */<br/>
      classLoader.loadClass(fullQualifiedName)<br/>
    }<br/>
  }<br/>
}<br/>
 <br/>
/*!### Compilation exception<br/>
 <br/>
  Compilation exception is defined this way.<br/>
  It contains program was compiling and error positions with messages<br/>
  of what went wrong during compilation.<br/>
 */
<br/>
class CompilationFailedException(val programme: String,<br/>
                                 val messages: Iterable[(Int, String)])<br/>
  extends Exception(messages.map(message => message._1 + ". " + message._2).mkString("n"))<br/>
 

Кстати, если заинтересуетесь форматом комментариев — это Circumflex Docco, отличная штука для наглядной документации.

Напоследок, неверно, необходимо сказать, что Scala быстро развивается и иногда разработчики меняют API. Данный код успешно опробован на версии 2.9.0.1, должен работать на всех 2.8.x и 2.9.x.
Теги:
Хабы:
Всего голосов 15: ↑13 и ↓2+11
Комментарии7

Публикации

Информация

Сайт
www.tinkoff.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия