[Перевод] Десять вещей, которые можно делать с GraalVM
От переводчика: GraalVM — новая, интересная технология, но на Хабре по ней не так много статей, которые бы могли показать примеры возможностей Graal. Статья ниже — это не просто перечисление того, что GraalVM умеет, но ещё и небольшой мастер-класс, аналогичный тому, который Chris Seaton и Олег Шелаев проводили на Oracle CodeOne 2018. Вслед за автором, призываю — пробуйте делать примеры из статьи, это действительно интересно.
В GraalVM много всего разного, и, если вы слышали это название раньше, или даже видели доклады, то все равно есть много вещей, о которых вы наверняка ещё не знаете, но которые GraalVM может делать. В этой статье мы рассмотрим разнообразные возможности, которые предоставляет GraalVM и покажем, что с их помощью можно сделать.
- Быстрое выполнение Java
- Уменьшение времени старта и потребления памяти для Java
- Комбинирование JavaScript, Java, Ruby и R
- Исполнение программ, написанных на платформо-зависимых языках
- Общие инструменты для всех языков программирования
- Дополнение JVM приложений
- Дополнение платформо-зависимых приложений
- Код Java как платформо-зависимая библиотека
- Поддержка нескольких языков программирования в базе данных
- Создание языков программирования для 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 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('' + color + '
');
});
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!
'
// Using Java standard library classes
text += BigInteger.valueOf(10).pow(100)
.add(BigInteger.valueOf(43)).toString() + '
'
// 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
#include
#include
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
#include
#include
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 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 есть учебник, который тоже можно изучить и выполнить упражнен