GraalVM: Programação poliglota com Java e Python

Fabio Henrique
Troopers-Legacy
Published in
12 min readMar 22, 2024
Fonte: Freepik.

Faaala seus pextes, tudo bom? Espero que sim!

O papo hoje é sobre um dos principais recursos da GraalVM: Programação poliglota. Vamos abordar o conceito de programação poliglota junto com Polyglot API, truffle, interoperabilidade e incorporabilidade. Por fim, traremos alguns exemplos utilizando a linguagem Python (Guest) interoperando com Java (Host).

Para quem não está familiarizado com a GraalVM, deixo o artigo GraalVM: Uma nova JVM realçada para Cloud-Native para leitura, facilitando o entendimento do que será dito adiante. #Let’s Booooora?!

Programação poliglota

Programação a gente já sabe o que é, né?! E “poliglota”? De acordo com o (Dicionário Michaelis, 2023), uma definição simples para esse termo seria:

1. Que ou aquele que sabe ou fala muitas línguas.
2. Que está escrito em muitas línguas.

Juntando os termos “Programação + Poliglota” podemos interpretar para outro termo mais conhecido: Multilinguagem. Basicamente, é ter um programa que pode ser escrito em várias linguagens (no mínimo duas) que se integram. Com base nos meus poucos anos de experiência, posso dizer que já vi algo parecido com isso quando programei com PHP Spaghetti — junção das tecnologias PHP + JavaScript + Css* = 😀- porém, de um jeito não tão harmônico.

Pensando em um modelo harmonioso, a GraalVM dedicou-se a criar uma plataforma totalmente flexível e adaptável. Para contextualizar, houve mudanças a nível de compilador (Graal Compiler) e criação de novos componentes, como Truffle, Sulong e Substrate VM.

Nas próximas seções será abordado o componente Truffle e como as multilinguagens se comportam por meio da interoperabilidade e incorporabilidade.

Nota: Embora Css não seja considerada uma linguagem de programação, foi adicionada no contexto para enfatizar o “mix” do que era a programação com PHP no passado distante.

Truffle

Figura 1Arquitetura GraalVM. Fonte: Autor.

Truffle Language Implementation Framework, mais conhecido como Truffle, é o componente responsável por habilitar a criação de interpretadores de linguagens que podem interagir entre si (Interoperabilidade e Incorporabilidade), além de estender outros recursos, tais como Graal Compiler e SubstrateVM (visíveis na Figura 2). Esses interpretadores permitem a execução de linguagens na GraalVM. O Truffle também é utilizado para desenvolver ferramentas, como debuggers e profilers.

É válido destacar que o Truffle é uma biblioteca integrada (mas desacoplada) na GraalVM (em ambas versões) e permite que os interpretadores de linguagens criados interpretem (a nível de compilador — Graal Compiler) os códigos baseado em Abstract Syntax Tree (AST). AST e Truffle serão assuntos abordados em posts futuros. Por hora, vamos apenas enfatizar o entendimento do Truffle no que se refere a programação poliglota.

A imagem a seguir exibe a execução de uma linguagem com interpretador implementado utilizando Truffle na GraalVM.

Figura 2Pilha de execução de uma linguagem com implementação de interpretador utilizando Truffle. Fonte: Autor.

Esta imagem mostra como funciona a pilha de execução de linguagens interpretadas por uma implementação baseada no Truffle. A primeira camada (Guest Language Application) é a linguagem escolhida para ser interpretada. Neste momento, nos referimos à linguagem original da escolha, por exemplo, PHP. Sendo assim, questões de sintaxe referentes ao código PHP permanecem sendo escritas da mesma maneira. Na segunda camada (Implementation Interpreter Guest Language by Truffle) é onde criamos, de fato, a implementação do interpretador para a linguagem escolhida. Aqui acontece “a mágica” 😀. Conforme documentação e declaração dos fundadores, o Truffle simplifica a implementação de linguagens sem que programadores se preocupem com questões de funcionalidade de baixo nível, como gerenciamento de memória. Logo, não é necessário criar um “compilador” — tarefa que pode ser extremamente complexa, custosa e lenta — uma vez que Truffle provê um framework para construir um interpretador AST. As duas últimas camadas são implementações dos componentes da GraalVM.

Visite a página “SimpleLanguage” para conferir uma demonstração de implementação.

Interoperabilidade & Incorporabilidade

Essas são duas características que caminham lado a lado no que se refere à programação com multilinguagens no Truffle. No contexto, interoperabilidade é a capacidade de fazer com que diferentes linguagens conversem entre si usando funcionalidades e transmitindo dados no mesmo espaço de memória em um único ambiente. Isso é resultado do Truffle e do Graal Compiler! A imagem a seguir apresenta em uma perspectiva ampla a implementação dos interpretadores e como acontece a intercomunicação entre eles.

Figura 3 Camada que unifica interoperabilidade entre as linguagens. Fone: Autor.

Já a incorporabilidade é a habilidade de permitir que códigos escritos em outras linguagens sejam embutidos (runtime) no mesmo programa. É como criar um método em um script Python e disponibilizá-lo para ser utilizado no Java, como ilustra a Figura 4.

Figura 4 Incorporando código. Fonte: Autor.

Como o objetivo deste artigo não é aprofundar os conceitos do Truffle, não vamos imergir profundamente nos mecanismos e implementações dos interpretadores. Entretanto, nas próximas seções serão introduzidos conceitos de interoperabilidade e incorporabilidade em exemplos práticos com Java e Python.

Projeto GraalPy

O projeto GraalPy é uma implementação da linguagem Python em cima da GraalVM. A versão atual (23.1.2) fornece um ambiente de execução compatível com Python 3.10.

É sabido que muitos pacotes, como NumPy e Pandas, implementam códigos C/C++ por questões de desempenho. No GraalPy, esses códigos C/C++ são executados por meio da GraalVM LLVM runtime 🙂.

Instalando o GraalPy

Principais referências de instalação:

Opções de instalação:

Pyenv

pyenv install graalpy-23.1.0

Conda-Forge

conda create -c conda-forge -n graalpy graalpy

Windows

Siga as instruções desta página https://www.graalvm.org/latest/docs/getting-started/windows/

Instalação de ambiente e pacotes Python

graalpy -m venv .env

source .env/bin/activate

graalpy -m pip install -r requirements.txt or graalpy -m ginstall install <package>==<version>

Incorporando Python no Java (Host)

Agora que entendemos o contexto sobre programação poliglota e como é o comportamento Truffle do na GraalVM, vamos para a parte prática deste artigo? Sei que você estava esperando por isso. 😏

Dependências (Java)

É claro que precisaremos usar a GraalVM para executar nossos exemplos, sendo assim, deixo aqui uma sugestão de como instalar.

Nota: Você sabia que é possível usar recursos de programação poliglota (Truffle) na OpenJDK ? Lembra que mencionamos que o truffle é uma biblioteca (desacoplada)? Pois é! Isso possibilita o uso na OpenJDK + Graal, uma vez que a GraalVM CE & EE são baseadas, respectivamente, nas versões OpenJDK e Oracle JDK. Essa informação abre precedente para uma questão muito provocativa: Qual a diferença de usar GraalVM comparado com OpenJDK + Graal, no que diz respeito a aplicações de longa duração? A resposta é que não há substanciais diferenças quando comparado com à compilação just-in-time (JIT), entretanto, a GraalVM inclui mecanismos adicionais para criar imagem nativa Ahead-of-time (AOT), trazendo baixo consumo de recursos computacionais. Indico a leitura da thread “Truffle on stock OpenJDK >= 11” para mais informações sobre o questionamento acima. É importante ressaltar que há projetos em andamento para trazer features do GraalVM para OpenJDK.

Com relação às dependências do projeto, segue configs Maven:

<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>${graalvm.version}</version>
</dependency>
<!-- Select language: js, ruby, python, java, llvm, wasm, languages-->
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>python-community</artifactId>
<version>${graalvm.version}</version>
<type>pow</type>
</dependency>

Há a possibilidade de utilizar as dependências que não contém o sufixo ‘-community’ e que usam licença “GraalVM Free Terms and Conditions (GFTC)”.

Para aplicações Java que fazem uso de módulos, é necessário importar o módulo: org.graalvm.polyglot.

module java.polyglot.with.python {
requires org.graalvm.polyglot;
}

Incorporando Python (Guest) no Java (Host)

Anteriormente mencionamos os termos Host e Guest. Eles são amplamente utilizados na literatura referencial (documentação) deste trabalho. Resumidamente, linguagens Guest são implementadas com o Truffle e linguagens Host são JVM-based (Java, Kotlin, Scala, Groovy, entre outras). Daqui em diante usaremos estes termos.

Nas próximas subseções serão explanadas algumas das principais funcionalidades da incorporação e interoperabilidade com trechos de códigos Java e Python.

Contexto

public static void main(String[] args) throws Exception {
try (Context ctx = Context
.newBuilder()
.allowAllAccess(true) /* Take care! */
.build()) {
ctx.eval("python", "print('Hello world - JS')");
}
}

Código 1

O clássico Hello World 😀! Context ctx é a referência do objeto que terá a “conexão” da comunicação entre o Host (Java) e Guests (Python, ou outras que serão citadas). É neste objeto que definimos configurações, como segurança (permitir acesso poliglota entre linguagens, I/O, threads, isolamento), cache de código, engine de execução, passagem de parâmetro para engine, compartilhamento de múltiplos contextos, entre outras definições.

Tome cuidado ao utilizar este recurso! Vários aspectos podem causar problemas ao utilizar Context. Lembre-se sempre de utilizar aplicando try-with-resource para evitar complicações relacionadas aos recursos “abertos”. Outra dica é utilizar o método allowAllAccess(true) com cautela, principalmente quando é necessário manter um contexto de execução mais restrito . Por exemplo, se for uma regra em que o contexto não possa criar threads, seja por questões de performance ou segurança, é fundamental configurá-los de acordo com as exigências.

Vale lembrar que é possível criar contexto com múltiplos Guests.

Context.newBuilder("python", "js")
.build();

Código 2

Acessando valores poliglotas com Classe Value

A classe Value representa o “valor” poliglota propriamente dito que estão vinculados a um Context. Uma Value pode representar vários tipos de dados, incluindo Null, String, Number (Integer, Long, Float, Double, …), Members (methods e fields acessíveis), Executable (function, method, closures ou promises), Iterator, Array Elements e outras opções que podem ser acessadas aqui.

Nos exemplos a seguir veremos a utilização da classe Value.

Avaliação de código (Evaluation)

Context.eval(Source) or Context.eval(String) são métodos que permitem avaliar código-fonte da linguagem Guest no contexto.

No código 1 vimos a utilização do Context.eval(String). O código a seguir exemplifica a utilização com Source.

import polyglot as poly

def add(n1, n2):
return n1 + n2

Código 3 — script-math.py

public static void main(String[] args) throws Exception {
try (Context ctx = Context
.newBuilder("python")
.allowPolyglotAccess(PolyglotAccess.newBuilder()
.allowBindingsAccess("python")
.build())
.build()) {
URL urlScriptPython = HelloWorld.class
.getClassLoader()
.getResource(Paths.get("script-math.py").toString());
ctx.eval(Source.newBuilder("python", urlScriptPython).build());
Value methodAdd = ctx.getBindings("python").getMember("add");
Value result = methodAdd.execute(1, 10);
int anInt = result.asInt();
System.out.println(anInt);
}
}

Código 4

O arquivo script-math.py deve estar no classpath ao executar o código. Além disso, não esqueça de fornecer as permissões para operar com com a linguagem Guest (.allowPolyglotAccess(PolyglotAccess.newBuilder().allowBindingsAccess("python").build()).

Como intuito de praticidade (menos arquivos e códigos separados), a maioria dos exemplos de códigos deste post será utilizando Context.eval(String). Em aplicações do mundo real é interessante que o código-fonte das linguagens Gest fiquem separados, como pode ser visto no exemplo acima. Isso facilita a legibilidade e a manutenibilidade do código. Um caso muito famoso que vimos na indústria de software foi o “PHP Spaghetti” 🤐.

Definindo, modificando e invocando (atributos e membros) do Guest no Host (Bindings)

public static void main(String[] args) throws Exception{
try (Context ctx = Context
.newBuilder()
.build()) {
final String PYTHON_SNIPPET_METHOD = """
def methodHelloSnippet():
print('Hello world - JS')
""";
ctx.eval("python", PYTHON_SNIPPET_METHOD);
Value methodHelloSnippet = ctx.getBindings("python").getMember("methodHelloSnippet");
System.out.println("Is it a method? " + methodHelloSnippet.canExecute())
methodHelloSnippet.execute();
}
}

Código 5

Após avaliar (Context.eval) o código fonte no contexto, é possível obter uma “ligação” de acesso com a linguagem Guest através do método Context.getBindings(String). Cada linguagem possui seu próprio object de “ligação” em um Context. Esse object de “ligação” pode ser usado para ler, inserir, modificar e deletar os membros (fields, methods) no escopo do contexto da linguagem Guest.

public static void main(String[] args) throws Exception{
try (Context ctx = Context
.newBuilder()
.build()) {
Value pythonBindings = ctx.getBindings("python");
pythonBindings.putMember("variable", "Put value by Host [Python Binding]");
final String PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT = """
def methodBindingWithPut():
print(f'#Python.print `variable`: {variable}') # variable pass by .putMember
""";
ctx.eval("python", PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT);
pythonBindings.getMember("methodBindingWithPut").execute();
}
}

Código 6

O exemplo acima mostra como inputar dados no contexto da linguagem Guest Python por meio do .putMember("variable", "Put value by Host.") e fazemos o print dessa variável no método methodBindingWithPut sem a necessidade de declarar no escopo a variável variable.

Há também a possibilidade de usar uma “ligação poliglota” fornecida pelo Context, que pode compartilhar valores entre as linguagens Guest definidas no escopo do contexto. Essa funcionalidade vai de encontro com o que foi exibido na Figura 3 sobre interoperabilidade entre multilinguagens. O código a seguir demonstra a interoperabilidade entre as linguagens Js e Python.

public static void main(String[] args) throws Exception{
try (Context ctx = Context
.newBuilder()
.allowPolyglotAccess(PolyglotAccess.newBuilder()
.allowBindingsAccess("python")
.allowBindingsAccess("js")
.build())
.build()) {
Value pythonBindings = ctx.getBindings("python");
Value jsBindings = ctx.getBindings("js");
Value polyglotBindings = ctx.getPolyglotBindings();
polyglotBindings.putMember("variable", "Put value by Host [Polyglot Binding].");

final String PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT = """
import polyglot as poly
def methodBindingWithPut():
variable = poly.import_value('variable') # needed to work
print(f'#Python.print `variable`: {variable}') # variable pass by .putMember
""";
ctx.eval("python", PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT);
pythonBindings.getMember("methodBindingWithPut").execute();
final String JAVA_SCRIPT_SNIPPET_METHOD_WITH_BINDING_PUT = """
const methodBindingWithPut = () => {
const variable = Polyglot.import('variable');
console.log(`#console.log \\`variable\\`: ${variable}`)
};
""";
ctx.eval("js", JAVA_SCRIPT_SNIPPET_METHOD_WITH_BINDING_PUT);
jsBindings.getMember("methodBindingWithPut").execute();
}
}

Código 7

Percebam que ao utilizar “ligações poliglotas” é preciso importar o membro que está sendo compartilhado para ser utilizado no escopo da linguagem. Caso as importações não sejam declaradas, será lançado:

org.graalvm.polyglot.PolyglotException: NameError: name 'variable' is not defined

Ainda com relação a “ligações poliglotas” é possível modificar membros compartilhados. O código a seguir altera o valor de `variable` através de uma das functions na linguagem Guest JavaScript e em seguida, é feito o print do valor no método da linguagem Guest Python. Vamos considerar uma classe Customizada (ObjectMetaDado) para esse exemplo, pois sabemos que String são imutáveis.

import org.graalvm.polyglot.HostAccess;
public class ObjectMetaDado {
@HostAccess.Export
public String id, module;
public ObjectMetaDado(String id, String module) {
this.id = id;
this.module = module;
}
}

Código 8 — Classe ObjectMetaDado.

public static void main(String[] args) throws Exception {
try (Context ctx = Context
.newBuilder()
.allowPolyglotAccess(PolyglotAccess.newBuilder()
.allowBindingsAccess("python")
.allowBindingsAccess("js")
.build())
.build()) {
Value pythonBindings = ctx.getBindings("python");
Value jsBindings = ctx.getBindings("js");
Value polyglotBindings = ctx.getPolyglotBindings();
polyglotBindings.putMember("variable", new ObjectMetaDado("#1", "Polyglot"));


final String PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT = """
import polyglot as poly
def methodBindingWithModify():
variable = poly.import_value('variable') # needed to work
variable.module = "[Polyglot Binding] #modify Python"
""";
ctx.eval("python", PYTHON_SNIPPET_METHOD_WITH_BINDING_PUT);
pythonBindings.getMember("methodBindingWithModify").execute();


final String JAVA_SCRIPT_SNIPPET_METHOD_WITH_BINDING_PUT = """
const methodBindingWithGet = () => {
const variable = Polyglot.import('variable');
console.log(`#console.log \\`variable\\`: ${variable.module}`)
};
""";
ctx.eval("js", JAVA_SCRIPT_SNIPPET_METHOD_WITH_BINDING_PUT);
jsBindings.getMember("methodBindingWithGet").execute();


System.out.println("Value variable.module: " + ctx.getPolyglotBindings()
.getMember("variable").getMember("module"));
}
}

Código 9

A classe ObjectMetaDado contém uma anotação @HostAccess.Export que permite que o contexto poliglota possa acessar o escopo da Host (respeitando inclusive os acessos a modificadores) e por esse motivo os membros id e module estão public (não recomendado). A utilização foi proposital para ressaltar que o encapsulamento é respeitado na interoperabilidade, sendo assim, se os atributos fossem private seria necessário dar permissão aos métodos getters & setters para manipular os valores do objeto. Ao fim deste post, no repositório do GitHub, será possível acessar outros exemplos implementados.

Usando bibliotecas externas do Python

Sabe-se que uma das grandes vantagens do Python é sua variedade de bibliotecas prontas para serem utilizadas 😀. Talvez aqui seja um bom motivo para adotarmos a utilização de programação poliglota com Python, principalmente quando falamos sobre bibliotecas como `numpy` — biblioteca de código aberto utilizada para trabalhar com arrays multidimensionais — que nos ajuda em tarefas relacionadas a machine learning.

Pensando em trazer um exemplo baseado em um cenário do mundo real, deixo aqui a implementação da utilização de um programa Java + Python (Pandas) cujo objetivo é analisar vários dados em um arquivo .csv .

Outras funcionalidades da interoperabilidade

Existem inúmeras características que podemos explicar sobre interoperabilidade entre Java (Host) e Python (Guest), como multithreads, tipos e conversões de dados suportadas, múltiplos contextos, cache code e muito mais.

Limitações do GraalPy

Como o projeto ainda está em fase experimental é importante tomar alguns cuidados, principalmente com os pacotes externos. O primeiro objetivo do time que está a frente da implementação do GraalPy foi assegurar a execução adequada do NumPy e pacotes relacionados usando Graal LLVM. Segundo FAQ do manual de referência do GraaPy, dos 500 principais pacotes PyPI, cerca de 50% atualmente passam na maioria dos testes.

Um outro ponto de atenção é com relação a interoperabilidade. O time tem realizado um trabalho contínuo com intuito de encontrar cenários ainda não tão claros de comportamentos inesperados na interoperabilidade com outras linguagens Guest.

Quando utilizar programação poliglota

Nunca podemos esquecer do motivo da tecnologia ter nascido. Assim vale para as linguagens de programação também! Partindo desse pressuposto, seja cauteloso ao utilizar programação poliglota com linguagens Guest na GraalVM.

Cenários em que se faz necessário uma transformação de tecnologia, reutilização de códigos já existentes e integração de componentes (legados, sistemas, bibliotecas) são casos pertinentes para utilizar programação poliglota.

É relevante destacar os aspectos de performance e limitações existentes. Por exemplo, não podemos esperar que o Python (GraalPy) seja performático tanto quanto o Python (CPython). Certamente há uma camada de abstração (Truffle) realizando a comunicação entre linguagens e tornando flexível a interoperabilidade. Dadas as circunstâncias, se a necessidade é utilizar o Python para adquirir performance (em linhas gerais, é um termo que se refere a desempenho a partir de uma perspectiva), pense bem em utilizá-lo com linguagem Guest. Talvez seja mais interessante utilizar Python para obter o máximo de performance possível, considerando que desempenho é um requisito não funcional da aplicação.

Conclusão

Programação poliglota é umas das principais features da GraalVM, talvez até a principal, na visão dos criadores. Neste post, abordamos os conceitos de programação multilinguagens e Truffle. Apresentamos também alguns exemplos de incorporabilidade de códigos Python no Java e interoperabilidade com códigos JavaScript, Python e Java. Além disso, discutimos em que situações é pertinente utilizar programação poliglota e como tirar proveito desta feature na GraalVM. E você, já usava a programação poliglota? Espero que esse artigo tenha te ajudado. Até a próxima.

O repositório GitHub a seguir contém algumas implementações das características mencionadas anteriormente. Fiquem a vontade para sugerir melhorias e propor contribuições!

--

--

Fabio Henrique
Troopers-Legacy

Software Engineer Java | Java 8, 11 & 17 Certified Developer.