GraalVM (II)

 

En el post anterior veíamos en qué consiste GraalVM. Comentábamos que, con esta máquina virtual, podemos ejecutar nuestras aplicaciones de lenguajes como Java, JS, Python, Ruby o incluso podrías desarrollar tu propio lenguaje o combinar estos lenguajes dentro de un único proceso. En este último punto será en el que nos centremos en este post. 

Para los ejemplos usaremos Java y JavaScript, lenguajes que suelo usar con mayor asiduidad, pero podéis encontrar ejemplos de otros lenguajes en los enlaces de documentación oficial.

 

architectureGraalVM

 

Todo lo que hemos comentado, es posible gracias al framework Truffle que, situado por encima de Graal (el compilador JIT) dentro de la arquitectura de GraalVM, es una librería que permite implementar lenguajes usando un intérprete de arboles de sintaxis abstracta (AST). A partir del cual, GraalVM puede generar un ejecutable (que realmente es una imagen nativa de las que hablaremos en el siguiente post).

Así pues, cuando usamos lenguajes invitados como JS (como vimos en el post anterior), Python o Ruby, estamos usando las implementaciones Java de éstos sobre Truffle, ya sea a través del ejecutable o embebiendo el intérprete en nuestra aplicación. Al ser implementaciones Java, la conexión es directa con la JVM y son fácilmente combinables a través de la API de poliglotismo, una de las características más llamativas de GraalVM. En nuestro caso usaremos la API Poliglota de Java.

En lo que a combinar lenguajes se refiere, GraalVM nos permite:

  • Escribir aplicaciones con lenguajes basados en la JVM (Java, Scala, Clojure...) que incluyen código de lenguajes invitados (JavaScript, Ruby, Python...). A ésto se refieren como embeber.
  • Compartir valores/objetos entre lenguajes de una manera directa sin tener que realizar copias o marshalling. Esto se denomina poliglotismo. Algo que hay que destacar es que, normalmente, que una aplicación sea políglota no suele tener un impacto en el rendimiento.

 

Código embebido

 

Empecemos viendo cómo podemos embeber código de un lenguaje invitado, como es JavaScript, en una aplicación Java.

Vamos a usar la clase Context del paquete Polyglot de GraalVM. Este contexto nos provee de un entorno de ejecución aislado para otros lenguajes. Con el método eval indicaremos primeramente qué lenguaje queremos usar y, a continuación, el código que queremos ejecutar:

 

import org.graalvm.polyglot.Context;

public class PolyglotDemo {
    public static void main(String[] args) {
        Context context = Context.create();
        context.eval("js", "console.log('Hello JS World!');");
        context.close();
    }
}

 

Como Context implementa AutoCloseable, podemos usar try de modo que inicialice el contexto y se cierre al finalizar. Cerrar el contexto es lo recomendado, aunque es algo opcional:

 

import org.graalvm.polyglot.Context;

public class PolyglotDemo {
    public static void main(String[] args) {
        try (Context context = Context.create()) {
            context.eval("js", "console.log('Hello JS World!');");
        }
    }
}

 

Compilamos y ejecutamos para ver el resultado:

 

bash-4.2# javac PolyglotDemo.java && java PolyglotDemo
Hello JS World!

 

Pues ya véis que se ejecuta código JavaScript dentro de un programa de Java.

 

Una ventaja de poliglotismo es, sin duda, la reutilización de librerías de otros lenguajes. Cuando se empezó a usar NodeJS, se veía como un punto fuerte que, entre otras cosas, se pudiese reusar código entre frontend y backend. Aquí le podríamos sacar partido del mismo modo.

Aunque hay que aclarar que justamente tenemos una limitación con JavaScript: solo está permitido usar JavaScript puro. Una de mis primeras intenciones era poder usar módulos de NodeJS, pero por ejemplo require no está permitido. Aquí podéis ver los detalles de la implementación de JavaScript y en ese issue de require indican que se podría usar load en su lugar.

De todos modos, la evolución de GraalVM es continua y quizás en el momento de lectura de este post, este punto ya este solucionado.

 

Poliglotismo

 

Veamos ahora un ejemplo de poliglotismo. Vamos a probar ahora a usar un tipo de Java del lado de JavaScript.

Por ejemplo, supongamos que tenemos la siguiente clase User y lo hemos compilado generando el .class:

 

public class User {

    private String name;

    public User(String name) {
        this.name = name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

 

Podríamos usar el tipo User desde el lado de JavaScript de la siguiente manera:

 

public class CustomTypeDemo {
    public static void main(String[] args) {
        try (Context context = Context.newBuilder()
                                      .allowAllAccess(true)
                                      .build()) {
            User user = context.eval("js",
                                     "const User = Java.type('User');" +
                                     "const newUser = new User('César');" +
                                     "newUser;")
                               .asHostObject();
            System.out.println(user.getName());
        }
    }
}

 

Primero estamos definiendo el contexto de una manera nueva, y es que a través del método allowAllAccess le estamos diciendo que va a tener acceso, entre otras cosas, a todas las clases públicas del host (un poco más adelante veremos más en detalle cómo definir otros tipos de acceso).

Ya dentro del código JavaScript obtenemos acceso al tipo User con Java.type. A partir de ahí, ya lo podemos usar en nuestro código. En la última línea de JavaScript se hace referencia al usuario creado a modo de return para poder obtenerlo, con asHostObject(), del lado de Java y poder seguir trabajando con él.

Antes de probarlo, comentar que el método eval devuelve un objeto Value de la SDK de GraalVM y es a traves del cual obtenemos el objeto User del lenguaje host, pero tambien podríamos obtener otros tipos como por ejemplo int, long, short o string a través de métodos como asInt, asLong, asShort o asString, respectivamente.

 

Ejecutamos para ver el resultado:

 

bash-4.2# javac User.java
bash-4.2# javac CustomTypeDemo.java && java CustomTypeDemo
César

 

Una vez más, podríamos tener definidos los tipos en un solo sitio y reutilizarlos.

 

Un ejemplo más avanzado

 

Hasta aquí bien, pero en la mayoría de los ejemplos que he visto ponen el código directamente en strings y no creo que sea lo que realmente se vaya a usar en el día a día. Vamos a ver un ejemplo donde leamos el código de un fichero de JavaScript y lancemos una de sus funciones desde Java.

Para el ejemplo, y enlazando con lo que comentábamos anteriormente de usar una misma librería para backend y frontend, tenemos un fichero que se llama mail-util.js y que dentro tiene una función que valida la dirección de email que se le pase.

 

function validateEmail(email) {
    var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}

 

Ahora veamos cómo poder usar esa función desde Java:

 

import java.io.File;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Value;

public class PolyglotDemo {
    public static void main(String[] args) {
        try(Context context = Context.create("js")) {

            // Obtenemos el fichero
            File file = new File(System.getProperty("user.dir"), 
                                 "mail-util.js");

            // Creamos el Source con nuestro fichero y se lo pasamos al Context
            String language = Source.findLanguage(file);
            Source source = Source.newBuilder(language, file)
                                  .build();
            context.eval(source);

            // Obtenemos la función como si de un valor se tratase a través del Binding
            Value jsFunction = context.getBindings(language)
                                      .getMember("validateEmail");

            // Ejecutamos la función pasándole la dirección que queremos validar
            System.out.println("isValid: " + 
                               jsFunction.execute("cesar@invalid")
                                         .asBoolean());
            System.out.println("isValid: " + 
                               jsFunction.execute("cesar@valid.com")
                                         .asBoolean());

        } catch(Exception e) {
            System.err.println(e.getMessage());
        }
    }
}

 

Primero obtenemos el fichero, a partir de este creamos el Source, que leerá el código, y lo evaluamos con el contexto. Finalmente, a través del contexto obtenemos la función y ya podemos ejecutarla:

 

bash-4.2# javac PolyglotDemo.java && java PolyglotDemo
isValid: false
isValid: true

 

Políticas de acceso

 

Antes comentábamos que se pueden definir las políticas de acceso y veíamos en el ejemplo que le dábamos acceso a todo. Pero se pueden crear políticas más restrictivas. Tenéis ejemplos en la propia API. como, por ejemplo:

 

HostAccess.newBuilder()
          .allowPublicAccess(true)       // Allows unrestricted access to all public constructors, methods or fields of public classes
          .allowAllImplementations(true) // Allow guest languages to implement any Java interface
          .allowArrayAccess(true)        // Allows the guest application to access arrays as values with array elements
          .allowListAccess(true)         // Allows the guest application to access lists as values with array elements
          .build();

 

Se puede dar acceso también solo a ciertos métodos/campos de una clase con la anotación HostAccess. Tenéis un ejemplo en la propia web, donde definen un servicio para crear empleados y una entidad empleado. Anotan con HostAcess.Export el método de creación de usuario y algún método de la entidad empleado para que estén accesibles desde JavaScript.

 

Os dejo aquí los enlaces a la documentación donde podéis encontrar más información y ejemplos:

 

 

Sulong

 

Pero también decíamos que GraalVM nos permitía usar lenguajes compatibles con la LLVM como C, C++ o Fortran. Este tipo de lenguajes usan Sulong: un intérprete de bitcode de la LLVM de alto rendimiento que, situado por encima de Truffle, nos permite ejecutar código de lenguajes de la LLVM. Al igual que el compilador Graal o Truffle, está escrito en Java. A diferencia de la compilación estática que se realiza para estos lenguajes, con GraalVM primero se interpreta y, a continuación, se compila dinámicamente con Graal.

 

Podemos probar el ejemplo de la documentación de GraalVM.

Creamos un fichero con código C donde mostremos por pantalla un mensaje:

 

#include <stdio.h>
    
int main() {
    printf("Hola Mundo C!!\n");
    return 0;
}

 

A continuación, generamos el bitcode y comprobamos que se ha generado el fichero .bc:

 

bash-4.2# clang -c -O1 -emit-llvm HelloWorld.c
bash-4.2# ls
HelloWorld.bc  HelloWorld.c

 

Ahora que ya hemos generado el bitcode, podemos ejecutar el programa con la herramienta lli:

 

bash-4.2# lli HelloWorld.bc
Hola Mundo C!!

 

En este punto os surgirá la duda de si es posible combinar C y Java al igual que antes hacíamos con JS y Java. Pues sí, es posible e igual de sencillo, salvo que en este caso necesitaremos el bitcode y no el código fuente.

 

import java.io.File;
import java.io.IOException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

class PolyglotDemo {
    public static void main(String[] args) throws IOException {
        try (Context context = Context.newBuilder()
                                      .allowAllAccess(true)
                                      .build()) {
            // Obtenemos el fichero
            File file = new File(System.getProperty("user.dir"), 
                                 "HelloWorld.bc");

            // Podríamos obviar este paso y poner directamente el valor "llvm"
            String language = Source.findLanguage(file);

            // Creamos el Source a partir de nuestro fichero
            Source source = Source.newBuilder(language, file)
                                  .build();

            // Se lo pasamos al contexto y finalmente ejecutamos
            Value program = context.eval(source);
            program.execute();
        }
    }
}

 

Seguimos los mismos pasos que estuvimos usando antes, pero en este caso indicaremos llvm en lo que a lenguaje se refiere y tendremos que leer el fichero HelloWorld.bc que generamos anteriormente.

Al probar a ejecutarlo vemos que funciona tal cual esperábamos.

 

bash-4.2# javac PolyglotDemo.java && java PolyglotDemo
Hola Mundo C!!

 

Os dejo aquí el enlace a la documentación donde podéis encontrar más información y ejemplos.

 

Conclusión

 

En este post hemos realizado unas pequeñas demostraciones de cómo embeber código de lenguajes invitados y crear aplicaciones políglotas. Hemos visto que se realiza de una manera sencilla. Y, finalmente, hemos echado un vistazo rápido a cómo podemos ejecutar lenguajes de la LLVM.

En el siguiente post descubriremos cómo crear imágenes nativas y podremos ver qué beneficios tienen. Algunas de estas imágenes nativas ya las hemos estado usando en este post y en el anterior, por ejemplo, cuando usamos el comando js desde línea de comando o justo en el punto anterior cuando hemos usado lli.

 

¡No pierdas la oportunidad de comentar si tienes algo que aportar o alguna duda... y suscríbete para estar al día!