В ноябре этого года вышел более-менее стабильный LTS-релиз GraalVM 20.3.0 от Oracle и я решил с ним поэкспериментировать. Для тех кто не в курсе, GraalVM позволяет использовать в едином окружении различные популярные языки программирования и обеспечивает их разностороннее взаимодействие в рамках некоторой общей среды выполнения. Платформа GraalVM вместе с исполняемой программой на смеси самых разных языков может быть представлена в виде автономного и самодостаточного исполняемого файла, либо работать поверх OpenJDK, Node.js или даже внутри Oracle Database. Наглядная схема из официальной документации:
https://www.graalvm.org/docs/img/graalvm_architecture.png
Поддержка гостевых языков осуществляется с помощью фреймворка Truffle, на основе этой библиотеки можно даже реализовать собственный язык программирования, который получит все плюшки платформы, вроде JIT-компиляции, многостороннего взаимодействия и прочего полезного. Из коробки в дистрибутиве GraalVM сразу присутствует возможность использования:
- Java, Kotlin, Scala и других языков JVM-платформы.
- JavaScript вкупе с Node.js и сопутствующим инструментарием.
- C, C++, Rust и других языков, которые могут быть скомпилированы в LLVM bitcode.
Экспериментальная поддержка заявлена для Python, Ruby, R и WebAssembly.
Собственно, хватит лирики, вот простенький прототип, использующий из экосистем JavaScript, Python и Ruby батарейки, реализующие server-side подсветку синтаксиса фрагментов исходного кода. Подобные библиотеки с богатым охватом языков программирования, которые они могут подсвечивать, отсутствуют на JVM-платформе:
// Highlighter.java, no comments, no checks.
// $ javac Highlighter.java
// $ jar -cvfe highlighter.jar Highlighter *.class
// $ cat hello.py | java -jar highlighter.jar rouge python
import org.graalvm.polyglot.Context;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class Highlighter {
private abstract class Highlight {
protected final Context polyglot =
Context.newBuilder("js", "python", "ruby").allowAllAccess(true).allowIO(true)
.build();
protected abstract String language();
protected abstract String renderHtml(String language, String rawCode);
protected String execute(String sourceCode) {
try {
return polyglot.eval(language(), sourceCode).asString();
} catch (RuntimeException re) { re.printStackTrace(); }
return sourceCode;
}
protected void importValue(String name, String value) {
try {
polyglot.getBindings(language()).putMember(name, value);
} catch (RuntimeException re) { re.printStackTrace(); }
}
}
private class Hjs extends Highlight {
@Override
protected String language() { return "js"; }
@Override
public String renderHtml(String language, String rawCode) {
importValue("source", rawCode);
String hjs = "";
try {
hjs = new Scanner(new File("highlight.min.js")).useDelimiter("\\A").next();
} catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); }
final String renderLanguageSnippet =
hjs + "\n" +
"hljs.highlight('" + language + "', String(source)).value";
return execute(renderLanguageSnippet);
}
}
private class Rouge extends Highlight {
@Override
protected String language() { return "ruby"; }
@Override
public String renderHtml(String language, String rawCode) {
importValue("$source", rawCode);
final String renderLanguageSnippet =
"require 'rouge'" + "\n" +
"formatter = Rouge::Formatters::HTML.new" + "\n" +
"lexer = Rouge::Lexer::find('" + language + "')" + "\n" +
"formatter.format(lexer.lex($source.to_str))";
return execute(renderLanguageSnippet);
}
}
private class Pygments extends Highlight {
@Override
protected String language() { return "python"; }
@Override
public String renderHtml(String language, String rawCode) {
importValue("source", rawCode);
final String renderLanguageSnippet =
"import site" + "\n" +
"from pygments import highlight" + "\n" +
"from pygments.lexers import get_lexer_by_name" + "\n" +
"from pygments.formatters import HtmlFormatter" + "\n" +
"formatter = HtmlFormatter(nowrap=True)" + "\n" +
"lexer = get_lexer_by_name('" + language + "')" + "\n" +
"highlight(source, lexer, formatter)";
return execute(renderLanguageSnippet);
}
}
public String highlight(String library, String language, String code) {
switch (library) {
default:
case "hjs": return new Hjs().renderHtml(language, code);
case "rouge": return new Rouge().renderHtml(language, code);
case "pygments": return new Pygments().renderHtml(language, code);
}
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in).useDelimiter("\\A");
if (scanner.hasNext()) {
String code = scanner.next();
if (!code.isEmpty()) {
System.out.println(new Highlighter().highlight(args[0], args[1], code));
}
}
}
}
Как видно, тут смешаны сразу четыре разных языка программирования. Утилита принимает на вход stdin в виде текста исходного файла, передаваемые аргументы определяют используемую библиотеку для подсветки и язык фрагмента, затем подсвечивается код и выводится готовый HTML на stdout терминала.
Подсветка фрагментов кода на стороне сервера лишь простейший пример использования библиотек, которые недоступны для определённой платформы, но доступны на нескольких других. С тем же успехом можно рассматривать какую-нибудь гораздо более полезную научную батарейку, написанную на Python или R, что-нибудь из ML и т. д. Получается, что JVM-платформу со своей кучей библиотек мы можем обогатить батарейками из экосистем других языков программирования и использовать их эксклюзивные библиотеки, альтернативы которых просто недоступны на нашей платформе.
Рецепт установки GraalVM, поддержки языков и библиотек, компиляция и запуск прототипа:
curl -LOJ https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.3.0/graalvm-ce-java8-linux-amd64-20.3.0.tar.gz
# curl -LOJ https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.3.0/graalvm-ce-java11-linux-amd64-20.3.0.tar.gz
cd /opt/
sudo mkdir graalvm
sudo chown `whoami`:`whoami` graalvm
cd /opt/graalvm/
tar -xvzf ~/graalvm-ce-java8-linux-amd64-20.3.0.tar.gz
rm ~/graalvm-ce-java8-linux-amd64-20.3.0.tar.gz
export GRAALVM_HOME=/opt/graalvm/graalvm-ce-java8-20.3.0
export JAVA_HOME=$GRAALVM_HOME
export PATH=$GRAALVM_HOME/bin:$PATH
gu install python
gu install ruby
# /opt/graalvm/graalvm-ce-java8-20.3.0/jre/languages/ruby/lib/truffle/post_install_hook.sh
graalpython -m ginstall install setuptools
curl -LOJ https://github.com/pygments/pygments/archive/2.7.3.tar.gz
tar -xvzf pygments-2.7.3.tar.gz
cd pygments-2.7.3/
graalpython setup.py install --user
cd ..
rm -Rf pygments-2.7.3/ pygments-2.7.3.tar.gz
gem install rouge
curl -LOJ https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.4.1/highlight.min.js
javac Highlighter.java
jar -cvfe highlighter.jar Highlighter *.class
cat hello.py
#!/usr/bin/env python
print("Hello, World!")
cat hello.py | java -jar highlighter.jar hjs python
<span class="hljs-comment">#!/usr/bin/env python</span>
print(<span class="hljs-string">"Hello, World!"</span>)
cat hello.py | java -jar highlighter.jar rouge python
<span class="c1">#!/usr/bin/env python
</span><span class="k">print</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span>
cat hello.py | java -jar highlighter.jar pygments python
<span class="ch">#!/usr/bin/env python</span>
<span class="nb">print</span><span class="p">(</span><span class="s2">"Hello, World!"</span><span class="p">)</span>
Благодаря поддержке AOT-компиляции в GraalVM можно даже собрать автономный нативный исполняемый файл из JAR-пакета:
sudo yum install gcc glibc-devel zlib-devel libstdc++-static
gu install native-image
# gu rebuild-images polyglot libpolyglot
native-image --language:js -jar highlighter.jar
cat Highlighter.java | ./highlighter hjs java
Вместо Java VM там будет использована легковесная и низкоуровневая Substrate VM, но с преимуществами JIT-компилятора в случае AOT придётся попрощаться. Зато запуск просто молниеносный, как у всех нативных программ. Стоит отметить, что пока у меня удалось сформировать подобный исполняемый файл лишь для связки JavaScript + Java. Создание подобных нативных образов довольно продолжительная и ресурсоёмкая операция, особенно по памяти. Для сборки примера потребовалось где-то 6 GB RAM, а в более сложных случаях требуется и целых 20 GB RAM.
Прототип постепенно оброс разной функциональностью и благодаря фреймворку Spring, который вполне себе работает на платформе GraalVM, превратился в простенький pastebin-сайт на котором можно обмениваться фрагментами исходного кода.
Все исходники и рецепты я выложил на GitHub: https://github.com/EXL/CodePolyglot
На Хабре имеется скучная и длинная статья про мои изыскания, может кому-нибудь будет интересно её почитать: https://habr.com/ru/post/534044/
Потыкать палочкой прототип в виде сайтика на GraalVM и Spring Boot можно тут: https://code.exlmoto.ru/
Примечание: не факт, что я долго буду держать сайт в онлайне, так что не рассчитывайте сохранять там что-то ценное.
Мне интересно ближайшее будущее GraalVM, похоже Oracle настроен очень серьёзно. Пока проект позиционируется им как альтернативная и идеальная платформа для микросервисов, но уже сейчас его разработка имеет влияние и на классический OpenJDK, например, в релизе JDK 15 была дропнута поддержка JavaScript-движка Nashorn, а в качестве его замены Oracle предлагает попробовать именно GraalVM. Кто знает, вдруг GraalVM в будущем будет предлагаться в качестве рекомендуемой JVM-платформы по умолчанию вместо OpenJDK? Время нам покажет.
Предлагаю обсудить эту грандиозную затею Oracle, пригодится ли кому-нибудь использовать все эти фичи GraalVM на практике? На официальном сайте платформы есть интересное заявление о том, что наша отечественная социальная сеть «Одноклассники» уже использует GraalVM в продакшене для server-side рендеринга React.js, что позволяет добиться хорошей отзывчивости на медленных интернет соединениях.
P.S. Поздравляю с наступающим Новым Годом анонимных и зарегестрированных пользователей ЛОРа. Счастья и крепкого сибирского здоровья вам, ребята!