Originally published at deepu.tech.
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
- Drop in JDK replacement - Some benchmarks show GraalVM to be faster and less memory hungry than other JDK vendors, I haven't personally run any benchmarks
- Drop in NodeJS replacement - Uses GraalVM instead of V8 as the engine for NodeJS
- Faster runtime for Ruby and R than the default implementations
- Ahead-of-time(AOT) compiled native images
- Polyglot capabilities - Java(Any JVM language), JavaScript, Ruby, Python, R, C/C++/Rust(LLVM) along with language interoperability
- The Truffle Language Implementation framework to implement third-party language support
Install GraalVM
Before we start, let us setup GraalVM. I used SDKMAN, you can also follow this if you like to install it manually.
- 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.
- Install LLVM toolchain, Python and Ruby support
gu install llvm-toolchain
export LLVM_TOOLCHAIN=\$(lli --print-toolchain-path)
gu install python
gu install ruby
- 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
- Python: Filters out the Fibonacci numbers from the given input array
- JavaScript: Find the cube of each number in the output array from the previous step
- C++: Get the sum of the numbers in the output array from the previous step
- Rust: Find the cube-root of the number from the previous step
- Ruby: Find factorial of the number from the previous step
- 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:
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.
Top comments (4)
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. :)
Yes this has great potential
Why are crates disallowed when using Rust?
This is the craziest stuff I saw in the last months... I LOVE THAT β€οΈ