company_banner

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

    Итак, приступим пожалуй. Я люблю 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.
    • +11
    • 7,4k
    • 7
    Tinkoff.ru
    149,00
    IT’s Tinkoff.ru — просто о сложном
    Поделиться публикацией

    Похожие публикации

    Комментарии 7

      0
      Scala мощный язык, однако порог вхождения очень высок.
      Проходит достаточно много времени прежде чем хороший кодер java сможет писать вменяемый код scala.
      А нет ли опасений, что созданный «ДСЛ»для текущей бизнес логики не сможете поддерживать если основной разработчик вдруг уволится?
      Ковырять чужой код Scala это как пигмею племени «Бегущего дождя» учить китайский.
        0
        У нас не DSL для бизнес-логики, пока пишем на Scala в ограниченных местах в Java-стиле. Получаем от Scala удобные утилиты для работы с коллекциями и вывод типов.

        У нас все разработчики основные. Ещё несколько человек с энтузиазмом приняли идею писать на Scala. Вместе с описанием техник в базе знаний всё не так трагично ;-)
          0
          Чем выше порог вхождения, тем меньше войдёт тех, кому не надо (гуглить по слову «PHP»).
          +2
          *беспомощно
          Тинькофф Кредитные Системы?
            +1
            * Глядя на текст поста
            А почему нет? Подобным компаниям есть очень много чего рассказать нашей аудитории.
            +1
            Когда-то давно натыкался на подобный утильный класс от твиттера: github.com/twitter/util/blob/master/util-eval/src/main/scala/com/twitter/util/Eval.scala

            И кстати, с шестой джавы есть Compiler API, так что подобные вещи можно делать и в джаве, только с бОльшим извратом.
              0
              Спасибо, не знал

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое