От переводчика: GraalVM — новая, интересная технология, но на Хабре по ней не так много статей, которые бы могли показать примеры возможностей Graal. Статья ниже — это не просто перечисление того, что GraalVM умеет, но ещё и небольшой мастер-класс, аналогичный тому, который Chris Seaton и Олег Шелаев проводили на Oracle CodeOne 2018. Вслед за автором, призываю — пробуйте делать примеры из статьи, это действительно интересно.


В GraalVM много всего разного, и, если вы слышали это название раньше, или даже видели доклады, то все равно есть много вещей, о которых вы наверняка ещё не знаете, но которые GraalVM может делать. В этой статье мы рассмотрим разнообразные возможности, которые предоставляет GraalVM и покажем, что с их помощью можно сделать.


  1. Быстрое выполнение Java
  2. Уменьшение времени старта и потребления памяти для Java
  3. Комбинирование JavaScript, Java, Ruby и R
  4. Исполнение программ, написанных на платформо-зависимых языках
  5. Общие инструменты для всех языков программирования
  6. Дополнение JVM приложений
  7. Дополнение платформо-зависимых приложений
  8. Код Java как платформо-зависимая библиотека
  9. Поддержка нескольких языков программирования в базе данных
  10. Создание языков программирования для GraalVM

Вы можете сделать все то, что показано в этой статье, при помощи GraalVM 1.0.0 RC1, которая доступна по ссылке с сайта GraalVM. Я использовал версию Enterprise Edition на MacOS, но код, который здесь написан, будет работать на Linux и на GraalVM Community Edition.


Когда будете читать статью, запускайте программы, которые в ней описаны! Код можно скачать с GitHub.


Установка


После загрузки с http://graalvm.org/downloads я добавил путь к исполняемым файлам GraalVM в $PATH. По умолчанию это добавляет поддержку выполнения Java и JavaScript.


$ git clone https://github.com/chrisseaton/graalvm-ten-things.git
$ cd foo
$ tar -zxf graalvm-ee-1.0.0-rc1-macos-amd64.tar.gz
    # or graalvm-ee-1.0.0-rc1-linux-amd64.tar.gz on Linux
$ export PATH=graalvm-1.0.0-rc1/Contents/Home/bin:$PATH
    # or PATH=graalvm-1.0.0-rc1/bin:$PATH on Linux

GraalVM идет со встроенной поддержкой JavaScript и содержит менеджер пакетов, который называется gu, добавляющий возможность установить поддержку других языков, помимо Java и JavaScript. Я дополнительно установил Ruby, Python и R, они скачиваются с GitHub.


$ gu install -c org.graalvm.ruby
$ gu install -c org.graalvm.python
$ gu install -c org.graalvm.R

Теперь, если вы выполните команду java или js, то увидите GraalVM версии этих движков.


$ java -version
java version "1.8.0_161"
Java(TM) SE Runtime Environment (build 1.8.0_161-b12)
GraalVM 1.0.0-rc1 (build 25.71-b01-internal-jvmci-0.42, mixed mode)
$ js --version
Graal JavaScript 1.0 (GraalVM 1.0.0-rc1)

1. Быстрое выполнение Java


“Graal” в GraalVM — это название компилятора. Он один создан, чтобы править всеми! Это означает, что это одна реализация компилятора, написанная в виде библиотеки, которая может быть использована для большого количества разных вещей. Например, мы используем Graal для компиляции как ahead-of-time, так и just-in-time, чтобы компилировать код, написанный на разных языках программирования, в том числе и для разных архитектур процессоров.


Первый, и самый простой способ использования Graal — это использовать его как Java JIT компилятор.


В качестве примера будем использовать программу, которая выдает 10 наиболее часто встречающихся слов в документе. В программе используются возможности современного языка Java, такие как Streams и коллекции.


import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TopTen {

    public static void main(String[] args) {
        Arrays.stream(args)
                .flatMap(TopTen::fileLines)
                .flatMap(line -> Arrays.stream(line.split("\\b")))
                .map(word -> word.replaceAll("[^a-zA-Z]", ""))
                .filter(word -> word.length() > 0)
                .map(word -> word.toLowerCase())
                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
                .entrySet().stream()
                .sorted((a, b) -> -a.getValue().compareTo(b.getValue()))
                .limit(10)
                .forEach(e -> System.out.format("%s = %d%n", e.getKey(), e.getValue()));
    }

    private static Stream<String> fileLines(String path) {
        try {
            return Files.lines(Paths.get(path));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

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


$ javac TopTen.java

Если мы запустим команду java, которая включена в GraalVM, то JIT компилятор Graal будет использоваться автоматически — не нужно делать никаких дополнительных действий. Я буду использовать команду time для того, чтобы получить реальные данные о времени, которое было потрачено на выполнение программы от начала до конца, вместо того, чтобы разворачивать сложный микробенчмарк. Также будет использоваться большой объем входных данных чтобы не было никаких инсинуаций о сэкономленной паре секунд тут или там. Объем файла large.txt — 150 Мб.


$ make large.txt
$ time java TopTen large.txt
sed = 502701
ut = 392657
in = 377651
et = 352641
id = 317627
eu = 317627
eget = 302621
vel = 300120
a = 287615
sit = 282613
real 0m17.367s
user 0m32.355s
sys 0m1.456s

Graal написан на Java, а не на C++, как большинство остальных JIT компиляторов для Java. Мы думаем, что это позволяет нам улучшать его быстрее, чем существующие компиляторы, дополняя новыми мощными оптимизациями (такими, например, как частичный escape анализ), которые недоступны в стандартном JIT компиляторе для HotSpot.
И это может сделать ваши Java программы значительно быстрее.


В целях сравнения, чтобы выполнять программы без JIT компилятора Graal, я буду использовать флаг -XX:-UseJVMCICompiler. JVMCI — это интерфейс между Graal и JVM. А ещё можно запустить пример на стандартной JVM и сравнить результаты.


$ time java -XX:-UseJVMCICompiler TopTen large.txt
sed = 502701
ut = 392657
in = 377651
et = 352641
id = 317627
eu = 317627
eget = 302621
vel = 300120
a = 287615
sit = 282613
real 0m23.511s
user 0m24.293s
sys 0m0.579s

Этот тест показывает, что Graal выполняет нашу Java программу примерно за три четверти того времени, которое требуется, чтобы выполнить ее со стандартным HotSpot компилятором. Там, где мы считаем, что улучшение производительности на единицы процентов — значительное достижение, 25 % — это большое дело.


Twitter — единственная компания, на сегодняшний день, которая использует Graal на “боевых” серверах, и они говорят, что для них это оправдано, в терминах экономии реальных денег. Twitter использует Graal для исполнения приложений, написанных на Scala — Graal работает на уровне JVM байткода, т.е. применим для любого JVM языка.


Это первый вариант использования GraalVM — просто замена JIT компилятора на лучшую версию для ваших существующих Java приложений.


2. Уменьшение времени старта и потребления памяти для Java


Сильные стороны платформы Java особенно явно проявляются при работе с долго выполняющимися процессами и пиковыми нагрузками. Короткоживущие процессы, напротив, страдают от долгого времени запуска и относительно большого использования памяти.


Например, если мы запустим приложение из предыдущего раздела, подав ему на вход гораздо меньший объем входных данных — около 1Кб вместо 150Мб, то, похоже, это займет неразумно долгое время и довольно много памяти — порядка 60Мб, для того, чтобы обработать такой маленький файл. Используем параметр -l для распечатки объема используемой памяти в дополнение ко времени исполнения.


$ make small.txt
$ /usr/bin/time -l java TopTen small.txt
      # -v on Linux instead of -l
sed = 6
sit = 6
amet = 6
mauris = 3
volutpat = 3
vitae = 3
dolor = 3
libero = 3
tempor = 2
suscipit = 2
        0.32 real         0.49 user         0.05 sys
  59846656  maximum resident set size

GraalVM дает нам инструмент, который решает эту проблему. Мы сказали, что Graal -это библиотека-компилятор и он может быть использован многими различными способами. Один из них — компиляция ahead-of-time в платформозависимый исполняемый образ, вместо компиляции just-in-time во время выполнения. Это похоже на то, как работает обычный компилятор, например gcc.


$ native-image --no-server TopTen
   classlist:   1,513.82 ms
       (cap):   2,333.95 ms
       setup:   3,584.09 ms
  (typeflow):   4,642.13 ms
   (objects):   3,073.58 ms
  (features):     156.34 ms
    analysis:   8,059.94 ms
    universe:     353.02 ms
     (parse):   1,277.02 ms
    (inline):   1,412.08 ms
   (compile):  10,337.76 ms
     compile:  13,776.23 ms
       image:   2,526.63 ms
       write:   1,525.03 ms
     [total]:  31,439.47 ms

Эта команда создает платформозависимый исполняемый файл, который называется topten. Этот файл не запускает JVM, он не слинкован с JVM и он вообще никаким способом не включает в себя JVM. Команда native-image по-настоящему компилирует ваш Java код и Java библиотеки, которые вы используете, в полноценный машинный код. Для компонентов среды выполнения, таких как сборщик мусора, мы запускаем нашу собственную новую VM, которая называется SubstrateVM, которая, как и Graal, также написана на Java.


Если вы посмотрите на зависимости, которые использует topten, то увидите, что это только стандартные системные библиотеки. Мы можем перенести только один этот файл на систему, в которой даже никогда не была установлена JVM и запустить его там, чтобы проверить, что он не использует JVM или какие-либо другие файлы. Topten также достаточно маленький — исполняемый код занимает объем меньше 6 Мб.


$ otool -L topten    # ldd topten on Linux
topten:
 .../CoreFoundation.framework ...
 .../libz.1.dylib ...
 .../libSystem.B.dylib ...
$ du -h topten 
5.7M topten

Если мы запустим этот исполняемый файл, то мы увидим, что он запускается примерно на порядок быстрее и использует примерно на порядок меньше памяти, чем та же программа, запущенная под JVM. Запуск настолько быстр, что вы не заметите, сколько времени это заняло. Если будете использовать командную строку — вы не почувствуете той паузы, которая обычно присутствует, когда вы запускаете маленькую, короткоживущую программу под JVM


$ /usr/bin/time -l ./topten small.txt
sed = 6
sit = 6
amet = 6
mauris = 3
volutpat = 3
vitae = 3
dolor = 3
libero = 3
tempor = 2
suscipit = 2
        0.02 real         0.00 user         0.00 sys
   4702208  maximum resident set size

В утилите native-image есть некоторые ограничения. Так, во время компиляции у вас должны присутствовать все классы, также есть ограничения в области использования Reflection API. Зато присутствуют некоторые дополнительные преимущества перед базовой компиляцией, такие как выполнение статических инициализаторов во время компиляции. Таким образом, уменьшается количество работы, выполняемой каждый раз, когда приложение загружается.


Это второе применение GraalVM — распространение и выполнение существующих Java программ, с быстрым стартом и меньшим расходом памяти. Этот способ устраняет такие проблемы с конфигурацией, как поиск нужного jar во время выполнения, а также позволяет создавать Docker образы меньшего размера.


3. Комбинирование JavaScript, Java, Ruby и R


Наравне с Java, GraalVM включает новые реализации движков JavaScript, Ruby, R и Python. Они написаны с использованием нового фреймворка, который называется Truffle. Этот фреймворк делает возможным создание интерпретаторов языков, которые одновременно и простые, и высокопроизводительные. Когда вы пишете интерпретатор какого-либо языка с использованием Truffle, то он будет автоматически использовать Graal, чтобы обеспечить JIT компиляцию для вашего языка. Таким образом, Graal — не только JIT компилятор и AOT компилятор для Java, он также может быть JIT компилятором для JavaScript, Ruby, R и Python.


Поддержка сторонних языков в GraalVM нацелена на то, чтобы быть прозрачной заменой для существующих движков исполнения различных языков. Например, мы можем установить модуль “color” для Node.js:


$ npm install --global color
...
+ color@3.0.0
added 6 packages in 14.156s

Потом написать программу с использованием этого модуля для конвертирования RGB HTML цвета в HSL:


var Color = require('color');

process.argv.slice(2).forEach(function (val) {
  print(Color(val).hsl().string());
});

И запустить ее привычным способом:


$ node color.js '#42aaf4'
hsl(204.89999999999998, 89%, 60.8%)

Движки исполнения разных языков в GraalVM работают вместе — есть API, который позволяет вам запускать код из одного языка в программе, написанной на другом языке. А это позволяет вам писать мультиязычные программы — программы, написанные более чем на одном языке программирования.


Это может понадобиться, если вы пишете большую часть своей программы на одном языке, но хотели бы использовать библиотеку, написанную на другом языке программирования. Например, представим, что нам нужно написать приложение для конвертирования имени цвета из CSS в его числовое представление в Node.js, но мы хотим использовать библиотеку работы с цветом, написанную на Ruby, вместо того, чтобы самому писать конвертацию.


var express = require('express');
var app = express();

color_rgb = Polyglot.eval('ruby', `
  require 'color'
  Color::RGB
`);

app.get('/css/:name', function (req, res) {
  color = color_rgb.by_name(req.params.name).html()
  res.send('<h1 style="color: ' + color + '" >' + color + '</h1>');
});

app.listen(8080, function () {
  console.log('serving at http://localhost:8080')
});

В этом коде мы написали, что надо выполнить Ruby код как строку, но обратите внимание, что мы не делали тут многого — мы всего лишь подключили библиотеки и потом возвратили Ruby объект. В Ruby мы бы его использовали так: Color::RGB.by_name (name).html. Если вы посмотрите на то, как color_rgb используется далее в JavaScript, то вы увидите, что мы, фактически, вызываем эти же методы из JavaScript, хотя это Ruby объекты и методы. И мы передаем их как строки JavaScript и соединяем результат, который является Ruby строкой, с JavaScript строкой.


Установим обе зависимости — Ruby и JavaScript.


$ gem install color
Fetching: color-1.8.gem (100%)
Successfully installed color-1.8
1 gem installed
$ npm install express
+ express@4.16.2
updated 1 package in 10.393s

Затем нужно запустить node с парой дополнительных опций: --polyglot, чтобы сказать, что нам нужен доступ к другим языкам и --jvm, потому что исполняемый образ node по умолчанию не включает в себя ничего, кроме JavaScript.


$ node --polyglot --jvm color-server.js
serving at http://localhost:8080

А потом перейдем на URL http://localhost:8080/css/orange (или какой-нибудь другой цвет), как обычно, в своем браузере.

Давайте попробуем сделать пример посерьезнее, который использует больше языков и модулей.


В JavaScript нет поддержки очень больших целых чисел. Я нашел несколько таких модулей, как big-integer, но они все неэффективны, т.к. хранят компоненты числа в виде JavaScript чисел с плавающей точкой. Класс BigInteger в Java более эффективен, давайте используем его для того, чтобы сделать несколько арифметических операций с большими целыми.


В JavaScript также нет никакой встроенной поддержки для рисования графиков, тогда как R превосходно рисует графики. Давайте используем модуль svg из R, чтобы нарисовать точечный график тригонометрической функции в 3D пространстве.


В обоих случаях мы будем использовать API для поддержки мультиязычности из GraalVM (далее — Polyglot API) и мы сможем просто вставить результаты выполнения программ на других языках в JavaScript.


const express = require('express')
const app = express()

const BigInteger = Java.type('java.math.BigInteger')

app.get('/', function (req, res) {
  var text = 'Hello World from Graal.js!<br> '

  // Using Java standard library classes
  text += BigInteger.valueOf(10).pow(100)
          .add(BigInteger.valueOf(43)).toString() + '<br>'

  // Using R interoperability to create graphs
  text += Polyglot.eval('R',
    `svg();
     require(lattice);
     x <- 1:100
     y <- sin(x/10)
     z <- cos(x^1.3/(runif(1)*5+10))
     print(cloud(x~y*z, main="cloud plot"))
     grDevices:::svg.off()
    `);

  res.send(text)
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})

Откройте http://localhost:3000/ в своем браузере, чтобы увидеть результат:

Это третья вещь, которую мы можем делать с GraalVM — запускать программы, написанные на нескольких языках и использовать модули из этих языков вместе в одной программе. Мы представляем это как способ унификации сред выполнения и библиотек — вы можете использовать тот язык программирования, который, как вы думаете, лучше всего подходит для решения текущей задачи и любую библиотеку, которую хотите, вне зависимости от того, на каком языке программирования она написана.


4. Исполнение программ, написанных на платформо-зависимых языках


Ещё один язык, который поддерживает GraalVM — это C. GraalVM может исполнять код на C так же, как он исполняет программы, написанные на JavaScript и Ruby.


Что на самом деле GraalVM поддерживает — так это запуск кода, полученного в результате выполнения утилит LLVM, т.е. биткод, а не прямую поддержку C. Это значит, что можно использовать существующий инструментарий для языка C и других, которые поддерживают LLVM, таких как С++, Fortran и, потенциально, большее количество языков в будущем. Для простоты демонстрации я запускаю специальную версию gzip, которая собрана одним файлом (эту версию поддерживает Stephen McCamant). Это просто исходный код gzip и конфигурация autoconf, объединенные в один файл, для простоты. Мне пришлось пропатчить пару вещей, чтобы это заработало на macOS и с clang, но специально для поддержки GraalVM я ничего не делал.


Мы компилируем gzip, используя стандартный clang (компилятор LLVM для С) и хотим, чтобы он сделал нам LLVM биткод, а не платформо-зависимую сборку, потому что ее GraalVM не запустит. Я использую clang 4.0.1.


$ clang -c -emit-llvm gzip.c

И затем запускаем полученный результат, напрямую используя команду lli (интерпретатор биткода LLVM) из GraalVM. Давайте попробуем сжать файл, используя мой системный архиватор gzip, а затем разархивировать его, используя gzip, запущенный под GraalVM.


$ cat small.txt
Lorem ipsum dolor sit amet...
$ gzip small.txt
$ lli gzip.bc -d small.txt.gz
$ cat small.txt
Lorem ipsum dolor sit amet...

Реализации Ruby и Python в GraalVM используют эту же технику для запуска расширений, написанных на C для этих языков. Это значит, что вы можете запускать эти расширения внутри VM и это позволяет нам сохранять высокую скорость выполнения, даже если мы используем устаревшие платформо-зависимые интерфейсы расширений.


Это четвертый способ использования GraalVM — запуск программ написанных на платформо-зависимых языках, таких как C или C++, а также запуск расширений к таким языкам, как Python или Ruby, что не в состоянии делать JVM реализации этих языков, такие как JRuby.


5. Общие инструменты для всех языков программирования


Если вы программируете на Java, то, вероятно, вы используете очень высококачественные инструменты, такие как IDE, отладчики и профилировщики. Не все языки обладают таким набором инструментов, но вы можете получить такой набор, если пользуетесь языками, которые поддерживаются GraalVM.


Поддержка всех языков в GraalVM (кроме Java, на текущий момент) реализована с помощью общего фреймворка — Truffle. Это позволяет нам сделать функциональность, например, отладчика, один раз и использовать ее для всех языков.


Чтобы это попробовать, мы напишем простейшую программу — FizzBuzz, потому что она наглядная (печатает что-то на экране) и в ней есть четкие ветвления, которые используются только в некоторых итерациях. Таким образом, нам будет проще расставить точки останова. Начнем с реализации на JavaScript.


function fizzbuzz(n) {
  if ((n % 3 == 0) && (n % 5 == 0)) {
    return 'FizzBuzz';
  } else if (n % 3 == 0) {
    return 'Fizz';
  } else if (n % 5 == 0) {
    return 'Buzz';
  } else {
    return n;
  }
}

for (var n = 1; n <= 20; n++) {
  print(fizzbuzz(n));
}

Запускаем программу как обычно, используя утилиту js, под GraalVM.


$ js fizzbuzz.js
1
2
Fizz
4
Buzz
Fizz

Мы также можем запустить программу с флагом --inspect. Это даст нам ссылку, которую можно открыть в Хроме и остановить программу в отладчике.


$ js --inspect fizzbuzz.js
Debugger listening on port 9229.
To start debugging, open the following URL in Chrome:
    chrome-devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9229/6c478d4e-1350b196b409

Можно поставить точку останова в коде FizzBuzz и затем продолжить выполнение. Когда программа прервет выполнение, мы увидим значение переменной n в отладчике и можем продолжить выполнение программы или поизучать интерфейс отладчика.

Отладчик в Хроме обычно используется для JavaScript, но для GraalVM в JavaScript нет ничего отличного от других языков. Флаг --inspect также доступен и работает в реализации Python, Ruby и R. Я не буду показывать вам исходники каждой программы, но они запускаются точно так же и вы получаете такой же отладчик в Хроме для каждой из них.


$ graalpython --jvm --inspect fizzbuzz.py


$ ruby --inspect fizzbuzz.rb


$ Rscript --inspect fizzbuzz.r


Ещё один инструмент, который, возможно, вам знаком из Java — это VisualVM. Он предоставляет пользовательский интерфейс, используя который вы можете присоединиться к работающей JVM на вашей локальной машине или через сеть, чтобы проинспектировать разные аспекты выполнения программы, такие как использование памяти или потоков выполнения.


GraalVM включает VisualVM в виде стандартной утилиты jvisualvm.


$ jvisualvm &> /dev/null &

Если мы запустим VisualVM в то время, когда у нас работает Java программа TopTen, то мы сможем понаблюдать за использованием памяти или, например, сделать снимок содержимого памяти и посмотреть, какие типы объектов у нас используют память в куче.


$ java TopTen large.txt


Я написал эту программу на Ruby, чтобы сгенерировать немного мусора в памяти во время выполнения.


require 'erb'

x = 42

template = ERB.new <<-EOF
  The value of x is: <%= x %>
EOF

loop do
  puts template.result(binding)
end

Если вы запустите стандартную реализацию Ruby на JVM — JRuby, то будете разочарованы VisualVM, потому что увидите только внутренние Java объекты вместо объектов своего языка.


Если же вы используете версию Ruby для GraalVM, то VisualVM распознает объекты Ruby. Нам нужно использовать параметр --jvm, чтобы использовать VisualVM, т.к. она не поддерживает родные версии Ruby.


$ ruby --jvm render.rb

Можно, если нужно, посмотреть на снимок кучи с внутренними Java объектами, как мы видели раньше, или, на закладке Summary, можно выбрать Ruby Heap и посмотреть вместо этого на реальные Ruby объекты.

Фреймворк Truffle — это что-то вроде Nexus для языков и инструментов. Если вы создаете движок своего языка при помощи Truffle и вы делаете такие свои инструменты, как отладчик, с учетом API Truffle’а для инструментария, то каждый такой инструмент будет работать для любого языка, основанного на Truffle, и вам нужно будет написать его только один раз.


Итак, пятое применение GraalVM — это платформа для создания высококачественных инструментов для языков программирования, у которых не всегда есть хороший инструментарий. С помощью Truffle и GraalVM вы можете использовать другие средства разработки, такие как отладчик в Хроме или VisualVM.


6. Дополнение JVM приложений


Помимо самостоятельного исполнения, а также совместного, в мультиязычных программах, поддержка других языков может быть добавлена в Java приложение. Новый API в пакете org.graalvm.polyglot позволяет вам загружать и запускать код, написанный на других языках и использовать значения из них.


import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

public class ExtendJava {
    public static void main(String[] args) {
        String language = "js";
        try (Context context = Context.newBuilder().allowNativeAccess(true).build()) {
            for (String arg: args) {
                if (arg.startsWith("-")) {
                    language = arg.substring(1);
                } else {
                    Value v = context.eval(language, arg);
                    System.out.println(v);
                }
            }
        }
    }
}

Если используются команды javac и java из дистрибутива GraalVM, то импорты org.graalvm.* уже будут в вашем classpath, т.е. можно будет компилировать и запускать код без всяких дополнительных флагов.


$ javac ExtendJava.java
$ java ExtendJava '14 + 2'
16
$ java ExtendJava -js 'Math.sqrt(14)'
3.7416573867739413
$ java ExtendJava -python '[2**n for n in range(0, 8)]'
[1, 2, 4, 8, 16, 32, 64, 128]
$ java ExtendJava -ruby '[4, 2, 3].sort'
[2, 3, 4]

Движки, используемые для выполнения команд на разных языках — те же высокопроизводительные мультиязычные версии, которые запускаются при помощи команд node или ruby, исполняемые GraalVM.


Это шестой способ использования GraalVM — как единого интерфейса для встраивания кода на других языках программирования в ваше Java приложение. Polyglot API позволяет вам брать объекты “гостевого” языка и использовать их как Java интерфейсы, а также предоставляет более сложные способы взаимодействия.


7. Дополнение платформо-зависимых приложений


В GraalVM уже включена одна библиотека, собранная способом, указанным в заголовке — это библиотека, которая позволяет вам запускать код на любом языке, поддерживаемом GraalVM, из платформо-зависимых приложений. Например, такой JavaScript движок, как V8, интерепретатор Python — CPython зачастую встраиваются в другие программы, т.е. они могут быть слинкованы с другим приложением в виде библиотеки. GraalVM позволяет использовать любой язык во встроенном контексте таким же способом — при помощи связывания со своей Polyglot библиотекой.


Когда вы устанавливаете GraalVM, то эта библиотека там собрана и присутствует, но по умолчанию она включает только встроенную поддержку JavaScript. Вы можете пересобрать Polyglot библиотеку, чтобы включить другие языки, используя команду:


$ graalvm-1.0.0-rc1/Contents/Home/jre/lib/svm/bin/rebuild-images libpolyglot

Создадим простую программу на C, которая выполняет команды, написанные на любом языке, поддерживаемом GraalVM, которые переданы в командной строке. Мы собираемся сделать эквивалент нашей программы ExtendJava из предыдущего раздела, но с использованием С, как основного языка.


Код на С
#include <stdlib.h>
#include <stdio.h>

#include <polyglot_api.h>

int main(int argc, char **argv) {
  graal_isolate_t *isolate = NULL;
  graal_isolatethread_t *thread = NULL;

  if (graal_create_isolate(NULL, &isolate) != 0 || (thread = graal_current_thread(isolate)) == NULL) {
    fprintf(stderr, "initialization error\n");
    return 1;
  }

  poly_context context = NULL;

  if (poly_create_context(thread, NULL, 0, &context) != poly_ok) {
    fprintf(stderr, "initialization error\n");
    return 1;
  }

  char* language = "js";

  for (int n = 1; n < argc; n++) {
    if (argv[n][0] == '-') {
      language = &argv[n][1];
    } else {
      poly_value result = NULL;

      if (poly_context_eval(thread, context, language, "unicalc", argv[n], &result) != poly_ok) {
        fprintf(stderr, "eval error\n");
        return 1;
      }

      char buffer[1024];
      size_t length;

      if (poly_value_to_string_utf8(thread, result, buffer, sizeof(buffer), &length) != poly_ok) {
        fprintf(stderr, "to string error\n");
        return 1;
      }

      buffer[length] = '\0';
      printf("%s\n", buffer);

      poly_destroy_handle(thread, result);
    }
  }

  return 0;
}

Теперь скомпилируем и запустим этот код, используя системный компилятор С и прилинкуем polyglot библиотеку из GraalVM. И опять, заметьте, нам не нужна JVM.


$ clang -Igraalvm-1.0.0-rc1/Contents/Home/jre/lib/polyglot /
      -rpath graalvm-1.0.0-rc1/Contents/Home /
      -Lgraalvm-1.0.0-rc1/Contents/Home/jre/lib/polyglot /
      -lpolyglot extendc.c -o extendc
$ otool -L extendc
extendc:
 .../libpolyglot.dylib ...
 .../libSystem.B.dylib ...

$ ./extendc '14 + 2'
16
$ ./extendc -js 'Math.sqrt(14)'
3.7416573867739413
$ ./extendc -python '[2**n for n in range(0, 8)]'
[1, 2, 4, 8, 16, 32, 64, 128]

Это седьмая вещь, которую можно сделать при помощи GraalVM — использовать одну-единственную библиотеку в вашем приложении, чтобы внедрить любой язык, поддерживаемый GraalVM.


8. Код на Java как платформо-зависимая библиотека


У Java великолепная экосистема с большим количеством очень высококачественных библиотек, которые, зачастую, недоступны для других экосистем, включая платформо-зависимые приложения, а также другие управляемые языки программирования. Если вы захотите использовать Java библиотеку из платформо-зависимого приложения, то вы можете встроить JVM в это приложение, но это довольно быстро сделает его большим и сложным.


GraalVM позволяет вам взять Java библиотеку, готовую или самописную, и скомпилировать ее в отдельную платформо-зависимую библиотеку, чтобы потом переиспользовать ее в других программах для этой платформы. Как и в случае с компиляцией, который был рассмотрен ранее, скомпилированные таким образом Java библиотеки не требуют JVM.


Я написал приложение, которое использует отличную библиотеку Apache SIS для работы с геоданными, чтобы рассчитать кратчайшее расстояние (ортодромию) между двумя точками на Земле. Я использовал SIS 0.8, которую я закачал отдельно с http://sis.apache.org/ и вытащил из нее jar.


import org.apache.sis.distance.DistanceUtils;

public class Distance {

    public static void main(String[] args) {
        final double aLat   = Double.parseDouble(args[0]);
        final double aLong  = Double.parseDouble(args[1]);
        final double bLat   = Double.parseDouble(args[2]);
        final double bLong  = Double.parseDouble(args[3]);
        System.out.printf("%.2f km%n", DistanceUtils.getHaversineDistance(aLat, aLong, bLat, bLong));
    }

    public static double distance(IsolateThread thread, double aLat, double aLong, double bLat, double bLong) {
        return DistanceUtils.getHaversineDistance(aLat, aLong, bLat, bLong);
    }

}

Скомпилируем это обычным образом и используем, чтобы получить расстояние между Лондоном и Нью-Йорком


$ javac -cp sis.jar -parameters Distance.java
$ java -cp sis.jar:. Distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

Мы можем скомпилировать это в исполняемый файл, как мы делали для программы topten.


$ native-image --no-server -cp sis.jar:. Distance
...
$ ./distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

А можем собрать это как библиотеку, а не исполняемый файл. Чтобы это сделать, мы объявим один или несколько методов как @CEntryPoint


...
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.c.function.CEntryPoint;

public class Distance {

    ...

    @CEntryPoint(name = "distance")
    public static double distance(IsolateThread thread,
          double a_lat, double a_long,
          double b_lat, double b_long) {
        return DistanceUtils.getHaversineDistance(a_lat, a_long, b_lat, b_long);
    }

    ...

}

Нам не нужно менять команду javac, потому что GraalVM автоматически кладет нужные API в classpath. Cкомпилируем код в разделяемую библиотеку и автоматически сгенерированный файл заголовка.


$ native-image --no-server -cp sis.jar:. -H:Kind=SHARED_LIBRARY      -H:Name=libdistance
$ otool -L libdistance.dylib   # .so on Linux
libdistance.dylib:
 .../libdistance.dylib ...
 .../CoreFoundation.framework ...
 .../libz.1.dylib ...
 .../libSystem.B.dylib ...
$ du -h libdistance.dylib
4.8M libdistance.dylib

Теперь напишем маленькую программу на С, которая будет использовать эту библиотеку. Чтобы воспользоваться интерфейсом нашей библиотеки, нужно провести небольшую церемонию: так как VM нужно управлять кучей, потоками сборщиком мусора и другими сервисами, нужно создать экземпляр системы и сказать об этом нашему основному потоку.


#include <stdlib.h>
#include <stdio.h>

#include <libdistance.h>

int main(int argc, char **argv) {
  graal_isolate_t *isolate = NULL;
  graal_isolatethread_t *thread = NULL;

  if (graal_create_isolate(NULL, &isolate) != 0 || (thread = graal_current_thread(isolate)) == NULL) {
    fprintf(stderr, "initialization error\n");
    return 1;
  }

  double a_lat   = strtod(argv[1], NULL);
  double a_long  = strtod(argv[2], NULL);
  double b_lat   = strtod(argv[3], NULL);
  double b_long  = strtod(argv[4], NULL);

  printf("%.2f km\n", distance(thread, a_lat, a_long, b_lat, b_long));

  return 0;
}

Компилируем это с использованием стандартных системных инструментов, и можем запускать наш исполняемый файл (нужно установить LD_LIBRARY_PARTH=. на Linux)


$ clang -I. -L. -ldistance distance.c -o distance
$ otool -L distance
distance:
 .../libdistance.dylib ...
 .../libSystem.B.dylib ...
$ ./distance 51.507222 -0.1275 40.7127 -74.0059
5570.25 km

Это восьмой способ применения GraalVM — компилировать код java в платформо-зависимую библиотеку, которую потом можно использовать в приложениях без использования JVM


9. Поддержка нескольких языков программирования в базе данных


Одно из приложений для Polyglot библиотеки — это использование внутри БД Oracle. Мы использовали библиотеку для создания Oracle Database Multilingual Engine (MLE), который включает в себя поддержку исполнения языков на GraalVM и модулей SQL.


Например, у вас есть front-end, уже написанный на JavaScript и мы делаем валидацию email адресов, используя JavaScript модуль validator. Если у нас есть какая-то логика для того же приложения в БД, то она наверняка написана на SQL или PL/SQL. А нам бы хотелось использовать ровно тот же валидатор, чтобы гарантировать консистентность результатов.


Загрузим MLE как Docker образ отсюда:


https://oracle.github.io/oracle-db-mle/releases/0.2.7/docker/


Потом загрузим образ в Docker Daemon.


$ docker load --input mle-docker-0.2.7.tar.gz

Запустите образ, используя Docker, а затем, когда загрузка закончится (она может занять несколько минут), запустите Bash терминал внутри образа.


$ docker run mle-docker-0.2.7
$ docker ps
$ docker exec -ti <container_id> bash -li

Если сможем запустить sqlplus (интерактивный SQL инструмент), в терминале Bash, чтобы присоединиться к базе данных, то это значит, что база запущена и работает.


$ sqlplus scott/tiger@localhost:1521/ORCLCDB

Если все прошло успешно, выйдите из в sqlplus. Теперь, в терминале Bash в Docker, мы установим модуль валидатора и затем запустим команду dbjs, чтобы загрузить этот модуль в базу данных. Затем запустим sqlplus снова.


$ npm install validator
$ npm install @types/validator
$ dbjs deploy -u scott -p tiger -c localhost:1521/ORCLCDB validator
$ sqlplus scott/tiger@localhost:1521/ORCLCDB

После установки мы можем использовать функции из модуля validator как часть SQL выражения.


SQL> select validator.isEmail('hello.world@oracle.com') from dual;
VALIDATOR.ISEMAIL('HELLO.WORLD@ORACLE.COM')
-------------------------------------------
                                          1
SQL> select validator.isEmail('hello.world') from dual;
VALIDATOR.ISEMAIL('HELLO.WORLD')
--------------------------------
                               0

И это девятая вещь, которую можно делать с GraalVM — запускать языки, поддерживаемые GraalVM, внутри БД Oracle. Таким образом, можно использовать логику из front-end или back-end, но внутри базы данных, вместо того, чтобы всегда вытаскивать данные для обработки из базы на сервер приложений.


10. Создание языков программирования для GraalVM


Oracle Labs и наши коллеги из академической среды смогли создать новые высокопроизводительные реализации JavaScript, R, Ruby, Python и C относительно небольшой командой, потому что мы разработали фреймворк Truffle, который облегчает процесс создания языков для GraalVM.


Truffle — это java библиотека, которая помогает написать интерпретатор абстрактного синтаксического дерева (AST). Интерпретатор AST — возможно, самый простой способ реализации языка, потому что он работает напрямую с выходными данными парсера и не включает интерпретацию байткода или компиляцию, но этот подход, зачастую, довольно медленный. Мы скомбинировали это с техникой, которая называется частичным вычислением, которая позволяет Truffle использовать Graal для автоматической JIT компиляции языка, основываясь только на выходных данных вашего AST интерпретатора.


Вы можете использовать Truffle, чтобы создать для GraalVM свой собственный язык программирования, или высокопроизводительный вариант существующего языка программирования, или DSL. Мы много говорим о деталях Truffle и Graal в нашем проекте, но мы часто забываем упомянуть, что Truffle — это действительно простой путь реализации языка программирования для GraalVM. И после создания у вас будут такие вещи, как, например, отладчик, автоматически. Любой, кто только что закончил начальный курс по языкам программирования, будет иметь необходимый набор навыков для этой работы. Oracle labs сделал базовую версию Ruby быстрее, чем все предыдущие проекты, силами всего лишь одного практиканта всего за несколько месяцев.


У нас здесь не так много места, чтобы полностью показать создание языка, даже самого микроскопического, но SimpleLanguage — это учебник по созданию собственного языка при помощи Truffle, основанный на простом языке в стиле JavaScript. Для того, чтобы понять, как это работает, можете, например, посмотреть реализацию оператора if.


Другие языки, написанные с использованием Truffle людьми вне Oracle Labs, включают вариант SmallTalk, Newspeak и Lisp. В примере для Lisp есть учебник, который тоже можно изучить и выполнить упражнения из него.


Заключение


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


Чтобы попробовать GraalVM, сходите на http://www.graalvm.org/. Там есть ссылки на загрузки и документацию и больше примеров, подобных тем, которые были показаны в этой статье.


Попробуйте сделать те примеры, о которых мы говорили здесь и попробуйте с ними поиграть с ними и посмотреть, что ещё можно сделать. Дайте нам знать о том, как прошли эксперименты с GraalVM в вашем приложении и пришлите нам отзыв на @ChrisGSeaton или @shelajev.


Благодарности: Oleg Selajev, Olya Gupalo и Doug Simon

Комментарии (20)


  1. alexander_8901
    17.12.2018 20:14
    +1

    Звучит очень-очень заманчиво) Возможность подтянуть разные библиотеки из разных языков очень соблазнительна, правда как представлю проект, написанный 10-ю разными разработчикаи на 10-ти разных языках, который надо поддерживать, начинает глаз дергаться ..)


    1. a_belyaev Автор
      17.12.2018 20:20

      Ну не обязательно же все свое писать, можно готовые подключать, как в случае графиков на R :-)


  1. C4ET4uK
    17.12.2018 20:23

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


    1. CyberSoft
      17.12.2018 21:12

      Есть пример Spring + R (это демка с гитхаба). Spring вроде как использует рефлексию, или я ошибаюсь?


      1. C4ET4uK
        17.12.2018 22:04

        Так это же не нативный образ. Graal умеет собирать «честную» Java без ограничений и тогда дает буст перформанса. А может собрать ее в «нативный образ» тогда получится машинно-зависимый код, без JVM, но зато быстро стартующий. Так вот именно нативная сборка рефлексию не умеет.


    1. ris58h
      17.12.2018 22:15

      в нативном образе нельзя использовать рефлексию

      Я бы не был настолько категоричен.


      1. C4ET4uK
        17.12.2018 22:41

        Согласен, правильно будет «в нативной сборке использование рефлексии ограничено».
        Но лично мое ИМХО — использовать рефлексию настолько неудобно, что можно сказать, что ее использовать нельзя.


    1. Throwable
      18.12.2018 14:47

      Бесполезен. Рефлексия очень глубоко вросла в Java и сильно савязана с Java runtime. Любой фреймворк или практически любая библиотека так или иначе завязана на рефлексию, а без своей экосистемы Java практически бесполезна. Рефлексия не позволяет компилятору вычислить статически все дерево вызовов и слинковать статически только используемый код, как это делает С например. Именно поэтому в Java 9 придумали модули — чтобы хоть как-то обязать пользователя объявлять статически границы, и чтобы финальный бандл можно было порезать с 200-300Мб до 30-40Мб.


      Помимо рефлексии фреймворки еще делают такие безобразные вещи как classpath-scan, annotation processing и кодогенерация в рантайме, custom classloaders, etc… Так что много еще что отвалится.


      1. C4ET4uK
        18.12.2018 14:54

        Если мне нужно написать скрипт на 200 строчек и я знаю только java — то рефлексию я зачастую использовать не буду. Часто вы в утильных скриптах используете рантайм кодогенерацию и кастомные класслоадеры?


        1. Throwable
          18.12.2018 15:38

          Возьмите GCJ чтоли и будет вам счастье. GraalVM как бы не для этого.


  1. hMartin
    17.12.2018 23:55

    Получается, что нативный образ — это условный kotlin/native с сохранением большей части либ под jvm? Интересно, почему в него не смогли джетбрейнсы?


    1. Dveim
      18.12.2018 14:52

      Не совсем, KN представляет собой куда более узкоспециализированную и ограниченную штуку. А вообще, «давайте сделаем набор случайных несвязанных ad-hoc возможностей» — modus operandi котлина в целом.


  1. vics001
    18.12.2018 03:05

    Помню было очень востребовано преобразовать java-code в native: а) чтобы не переписывать кучу кода б) чтобы получить преимущества native.

    Все это упирается в то, что Java-код использует большую std-либу, как-то Calendar, Date, Locale, Collator (icu) — сложно представить программу c более 50К LOC, где не вылезет что-то такое. Так вот до GraalVM был многообещающий AvianVM c AOT: куча классных HelloWorld компилировалось, но нужный проект не взлетал, крешился при работе с данными и GС. Avian развивался 4 года, но так и не смог запустить не самую сложную Java программу, а потом похоже пропал интерес и все закончилось.

    Хочется верить, что GraalVM не ждет участь неудачников, ведь написание AOT крайне сложная задача, особенно если мы говорим о кроссплатформенной генерации ASM. Прежде всего хотелось бы, чтобы GraalVM позволял компилировать Java-code и делать удобную линковку с C++ кодом туда и обратно. Если это еще можно будет скомпилировать через clang для мобильных платформ. наступит сказка.


    1. Throwable
      18.12.2018 14:28

      Был когда-то больше 10 лет назад вполне рабочий GCJ. Писали на нем и утилиты и серверы. Все летало, особых багов замечено не было. А 20 лет назад был Microsoft JVM, который поддерживал нативную линковку с DLL-ями, win32 API и ActiveX. Еще есть отличный Excelsior…
      GraalVM ждет та же участь — мутное целевое использование, сильная завязанность на Java и ее ограничениях, запоздалость. Сейчас грядет WebAssembly, компиляторы в него из разных языков, затем его выпустят как отдельную VM.


  1. acmnu
    18.12.2018 09:07

    Нативный бинарник? Черт, я впервые почувствовал непреодолимое желание написать что-нибудь на Java. Это на столько хорошо, что не верится.


  1. potan
    18.12.2018 11:53

    Забавно было бы сделать на нем JIT .net…


    1. a_belyaev Автор
      18.12.2018 15:01

      Так берешь Truffle и строишь AST из C# — и будут программы, написанные на C# запускаться :-) Можно ещё попробовать CIL напрямую подсовывать в GraalVM, но это посложнее.


      1. potan
        18.12.2018 19:45

        Раз jvm и llvm поддерживаются, значит байткод туда тоже тоже можно засунуть. Просто C# не так интересно, хочется вызывать F# и Nemerle из Scala.


  1. alatushkin
    18.12.2018 12:44

    Есть информация по поводу нативных образов из других языков с биндингами?


    Просто тот же python конечно хорош, но часто его польза в большом количестве библиотек с привязкой нативных библиотек


    1. a_belyaev Автор
      18.12.2018 14:54

      Там как раз написано, что bitcode может выполняться совместно с python. Но совсем нативные образы не получится слинковать, мне кажется. Но, если библиотеки перекомпилировать, то должно все получиться.