loading...
Cover image for Polyglot inception with GraalVM. Why? Because it's fun 🏄

Polyglot inception with GraalVM. Why? Because it's fun 🏄

deepu105 profile image Deepu K Sasidharan Originally published at deepu.tech Updated on ・8 min read

Originally published at deepu.tech.

Please follow me on Twitter for updates and let me know what can be improved in the post.


Have you heard of GraalVM? If you haven't you should check it out. It is an exciting technology, you know the kind that gets a polyglot developer going 😉

From the website:

GraalVM is a universal virtual machine for running applications written in JavaScript, Python, Ruby, R, JVM-based languages like Java, Scala, Groovy, Kotlin, Clojure, and LLVM-based languages such as C and C++.

GraalVM is one of its kind. It is a polyglot VM developed at Oracle and apart from its polyglot capabilities it also has been proven to be quite performant and has a smaller memory footprint. It has support for building native images and some modern Java microservice frameworks like Micronaut and Quarkus support GraalVM as it provides faster startup and smaller footprint which is ideal in microservice architectures.

So what are the capabilities of GraalVM? Let us take a quick look

GraalVM features

Install GraalVM

Before we start, let us setup GraalVM. I used SDKMAN, you can also follow this if you like to install it manually.

  1. First install SDKMAN if you don't have it already
sdk list java
# you can use a newer version if available
sdk install java 19.3.1.r11-grl
sdk use java 19.3.1.r11-grl
# Check everything
java -version
node -v
lli --version

The above will install GraalVM and set it up as java, node and lli context. Please note: If you start a new terminal session, you would have to run sdk use java 19.3.1.r11-grl again.

  1. Install LLVM toolchain, Python and Ruby support
gu install llvm-toolchain
export LLVM_TOOLCHAIN=\$(lli --print-toolchain-path)

gu install python
gu install ruby
  1. Install Rust
curl https://sh.rustup.rs -sSf | sh

That's it we are ready to roll!

Lets have some fun

As a polyglot developer, GraalVM is quite interesting to me as I can use many languages I love together making use of the best parts from each. Let's explore the polyglot capabilities offered by GraalVM, please note that support for Python, Ruby, R, and Rust are still in the experimental stage and hence your mileage may vary. We will build a program today using Java, JavaScript, Ruby, Rust, Python, and C++.

I wanted to use Rust and Go as well. While Rust mostly works via the GraalVM lli command line, it has a lot of limitations when embedded in polyglot mode. After a lot of fiddling around, I did manage to get it working. For Golang, it might be possible with this Go LLVM compiler as shown here, but it's having its own set of issues as well when I tried. So I have given up on Golang for now. Let me know if any of you got it working.

We will have a simple(silly 😉) application written in Java that composes methods from different languages for each step from within Java

  1. Python: Filters out the Fibonacci numbers from the given input array
  2. JavaScript: Find the cube of each number in the output array from the previous step
  3. C++: Get the sum of the numbers in the output array from the previous step
  4. Rust: Find the cube-root of the number from the previous step
  5. Ruby: Find factorial of the number from the previous step
  6. Java: Finally print the result(this is also the wrapper program)

If you prefer a more complex example, check this out.

Step 1: Java

Let's start with our Java wrapper program Polyglot.java

import java.io.*;
import org.graalvm.polyglot.*;

class Polyglot {
    // We create a polyglot context to evaluate source files
    static Context polyglotCtx = Context.newBuilder().allowAllAccess(true).build();

    // Utility method to load and evaluate a source file
    static Value loadSource(String language, String fileName) throws IOException {
        File file = new File(fileName);
        Source source = Source.newBuilder(language, file).build();
        return polyglotCtx.eval(source);
    }

    // Utility method to convert arrays between languages
    static int[] getIntArrayFromValue(Value val) {
        int[] out = new int[(int) val.getArraySize()];
        if (val.hasArrayElements()) {
            for (int i = 0; i < val.getArraySize(); i++) {
                out[i] = val.getArrayElement(i).asInt();
            }
        }
        return out;
    }

    public static void main(String[] args) throws IOException {

        int[] input = new int[] { 4, 2, 8, 5, 20, 1, 40, 13, 23 };

        /* PYTHON: Get the Fibonacci numbers from the array */
        loadSource("python", "pythonpart.py");
        Value getFibonacciNumbersFn = polyglotCtx.getBindings("python").getMember("getFibonacciNumbers");
        int[] fibNumbers = getIntArrayFromValue(getFibonacciNumbersFn.execute(input));

        /* JAVASCRIPT: Find cube of numbers in the output array */
        loadSource("js", "jspart.js");
        Value findCubeOfNumbersFn = polyglotCtx.getBindings("js").getMember("findCubeOfNumbers");
        int[] sqrNumbers = getIntArrayFromValue(findCubeOfNumbersFn.execute(fibNumbers));

        /* C++: Get the sum of the numbers in the output array */
        loadSource("llvm", "cpppart");
        Value getSumOfArrayFn = polyglotCtx.getBindings("llvm").getMember("getSumOfArray");
        int sum = getSumOfArrayFn.execute(sqrNumbers, sqrNumbers.length).asInt();

        /* Rust: Find the cube root of sum */
        Value cubeRootFn = loadSource("llvm", "rustpart.bc").getMember("cube_root");
        // println! macro does not work from Rust when embedded, some issue with mangling
        System.out.println("Rust => Find cube root of the number");
        Double cubeRoot = cubeRootFn.execute(sum).asDouble();

        /* RUBY: Find factorial of the number */
        Value factorialFn = loadSource("ruby", "rubypart.rb");
        long out = factorialFn.execute(cubeRoot).asLong();

        System.out.println("Sum: " + sum);
        System.out.println("Cube Root: " + cubeRoot);
        System.out.println("Factorial: " + out);
    }
}

The utility functions are to simplify the code, now let's look at each step where it composes the functions.

Step 2: Python

We are executing the getFibonacciNumbers function located in file pythonpart.py and passing it our input array.

/* PYTHON: Get the Fibonacci numbers from the array */
loadSource("python", "pythonpart.py");
Value getFibonacciNumbersFn = polyglotCtx.getBindings("python").getMember("getFibonacciNumbers");
int[] fibNumbers = getIntArrayFromValue(getFibonacciNumbersFn.execute(input));

Let's look at pythonpart.py, it is a standard python program that takes an array and filters out the Fibonacci numbers from it and returns the resulting array.

import math

def isPerfectSquare(num):
    n = int(math.sqrt(num))
    return (n * n == num)

# Function to check if the number is in Fibonacci or not
def getFibonacciNumbers(array):
    print("Python => Filtering Fibonacci number from the array");

    out = []
    n = len(array)
    count = 0
    for i in range(n):
        if (isPerfectSquare(5 * array[i] * array[i] + 4) or
            isPerfectSquare(5 * array[i] * array[i] - 4)):

            out.append(array[i]);
            count = count + 1

    if (count == 0):
        print("None present");

    return out

Step 3: JavaScript

We are executing the findCubeOfNumbers function located in file jspart.js and passing the result from the Python function.

/* JAVASCRIPT: Find cube of numbers in the output array */
loadSource("js", "jspart.js");
Value findCubeOfNumbersFn = polyglotCtx.getBindings("js").getMember("findCubeOfNumbers");
int[] sqrNumbers = getIntArrayFromValue(findCubeOfNumbersFn.execute(fibNumbers));

Let's look at jspart.js, it is a standard JavaScript function that takes an array and maps over the elements and returns the array. But we had to call Array.prototype.map.call instead of just array.map as the array passed by Java is not standard JS array.

function findCubeOfNumbers(array) {
    console.log("JS => Getting cube of numbers in the array");

    return Array.prototype.map.call(array, it => Math.pow(it, 3));
}

Step 4: C++

We are executing the getSumOfArray function located in the cpppart binary file. We pass the result from JS function and the length of the array here. We have to use compiled binary here unlike Python, Ruby, and JavaScript which are interpreted languages.

/* C++: Get the sum of the numbers in the output array */
loadSource("llvm", "cpppart");
Value getSumOfArrayFn = polyglotCtx.getBindings("llvm").getMember("getSumOfArray");
int sum = getSumOfArrayFn.execute(sqrNumbers, sqrNumbers.length).asInt();

The source of the binary is in cpppart.cpp file. Which is compiled using the below

export LLVM_TOOLCHAIN=$(lli --print-toolchain-path)
$LLVM_TOOLCHAIN/clang++ -shared cpppart.cpp -lpolyglot-mock -o cpppart

Let's look at cpppart.cpp, it is a standard C++ program that exports a function. It takes an array and its length as the arguments and returns a number

#include<iostream>
using namespace std;

// Function to find the sum of integer array
// extern "C" is required to suppress mangling
extern "C" int getSumOfArray(int array[], int size) {
    printf("C++ => Find sum of numbers in an array\n");

    int i, sum = 0;
    for(i = 0; i < size; i++) {
        sum += array[i];
    }
    return sum;
}

Step 5: Rust

We are executing the cube_root function located in file rustpart.bc, compiled from rustpart.rs. We pass the result from C++ function here.

/* Rust: Find the cube root of sum */
Value cubeRootFn = loadSource("llvm", "rustpart.bc").getMember("cube_root");
// println! macro does not work from Rust when embedded, some issue with mangling
System.out.println("Rust => Find cube root of the number");
Double cubeRoot = cubeRootFn.execute(sum).asDouble();

Let's look at rustpart.rs, it is a standard Rust function that takes a number finds its cube root and returns it. But we do have to specify #[no_mangle] annotation and we cannot use any crates as well apparently. Simples functions with primitive args/output seem to work but more complex functions do not work when embedded.

#[no_mangle]
fn cube_root(arg: f64) -> f64 {
    arg.cbrt()
}

fn main(){}

We compile the Rust source to binary code using rustc compiler with the --emit=llvm-bc flag

rustc --emit=llvm-bc rustpart.rs

Step 6: Ruby

We are executing the factorial function located in file rubypart.rb. We are passing the result from the Rust function here.

/* RUBY: Find factorial of the number */
Value factorialFn = loadSource("ruby", "rubypart.rb");
long out = factorialFn.execute(cubeRoot).asLong();

Let's look at rubypart.rb, it is a standard Ruby lambda function that takes a number and returns its factorial.

factorial = -> (num) {
    puts "Ruby => Find factorial of the number"
    (1..num).inject {|product, num| product * num }
}

And Finally, we print the outputs with Java.

Run the program

To run this program we need to compile the C++, Rust and Java files first, and then run it using the JVM provided by GraalVM. Below are the steps, you can save this as run.sh and execute it.

export LLVM_TOOLCHAIN=$(lli --print-toolchain-path)
$LLVM_TOOLCHAIN/clang++ -shared cpppart.cpp -lpolyglot-mock -o cpppart || exit

rustc --emit=llvm-bc rustpart.rs || exit

javac Polyglot.java && java Polyglot

It will produce the below output:

Command output


Conclusion

Wasn't this fun? So is such a polyglot capability useful? Well that depends, the polyglot capabilities of GraalVM are still not production-ready but it is still useful as it opens up the door for real language interoperability, imagine being able to use a library from any language from your program, this is already possible for many C, Ruby, R, JS and Java libraries with GraalVM but as support becomes better we would be able to break free from being limited to one language. GraalVM seems to be much faster for languages like Ruby than the standard CRuby or JRuby for example and that is promising as it would mean you wouldn't have to worry much about overheads when embedding multiple languages in your program.

GraalVM is one of the most revolutionary technologies I have encountered in recent times and I hope the polyglot language support becomes production-ready soon combined with its native image capabilities it would be a very powerful platform for truly polyglot applications.


If you like this article, please leave a like or a comment.

You can follow me on Twitter and LinkedIn.

Cover image credit: Based on official logos of respective projects.

Discussion

pic
Editor guide
Collapse
markoshiva profile image
Marko Shiva Pavlovic

That are really some amazing stuff. I do like the idea of making a VM that can do all toolchains out of box. For VM deployments it might become default. :)

Collapse
deepu105 profile image
Deepu K Sasidharan Author

Yes this has great potential

Collapse
crinklywrappr profile image
Daniel Fitzpatrick

Why are crates disallowed when using Rust?

Collapse
jotafeldmann profile image
Jota Feldmann

This is the craziest stuff I saw in the last months... I LOVE THAT ❤️