; last updated - 15 minutes read

GraalVM allows you to run Java, JavaScript, Ruby, and a broad range of other programming languages. They all run in the same virtual machine. So let's do the next logical step. Let's write a polyglot, multilingual application.

Traditional languages compiling to Java bytecode

If you're using a language like Scala, Kotlin, Groovy, or Ceylon, you don't need the GraalVM to write a multilingual application. These languages compile to Java bytecode, so there's a common denominator. In theory, nothing stops a - say - Scala class from calling a Java method or a Groovy method. This approach works well. Currently, I'm using a mixed setup of Java 11 and Kotlin at work.

World of languages. Image published at freesvg.org under a CC0 license by j4p4nThere's just one little disadvantage. Every language has it's own data types. As long as you stick to the least common denominator, everything's fine, but more often than not, you have to convert an advanced data type of your pet language to a bread-and-butter Java type.

GraalVM doesn't change that. It just allows you to integrate many more languages running on Truffle. Languages like R, Ruby, JavaScript, and all those languages compiling to LLVM bitcode, and it promises to run them fast (or at least fast enough, as we've seen in the previous articles of this series). This article focuses on the non-bytecode languages and how they interoperate with Java.

Non-JVM languages aren't first-class citizens of Graal country

When I (Stephan) heard first about GraalVM and its approach to polyglot programming, I expected it to seamlessly integrate algorithms written in any combination of these languages. Mind you, you can use GraalVM to run a JavaScript program exactly as you'd do with node.js. Similarly, you can run a Ruby application with the command ruby myProgram.rb. There's no difference in using one of the traditional Ruby interpreters.

So I expected to be able to simply call a JavaScript function from Java, just as I'd call a method written in Kotlin. The JavaScript file is available in the project, so what stops GraalVM from running it?

To my disappointment, GraalVM doesn't work that way. Maybe there's simply no demand for this level of interoperability. Plus, you'd need a way to declare the JavaScript function to make them available to Java. In the case of Kotlin, that's easy: there's a pom.xml or build.gradle that links source codes from the two languages. At the end of the day, both Java and Kotlin files compile to bytecode, so calling one from the other is easy.

What polyglot programming looks and feels like

There's no such link between Java and the non-JVM language. So GraalVM uses a less ambitious approach. It provides an API you can use. The idea is you load the JavaScript file into a String and pass that String to Truffle. That's the same approach we already know from Nashorn and Rhino. Rhino is probably the better comparison because the API of Truffle resembles the API of Rhino.

Update March 07, 2021:

The disadvantage is there's no require statement. If you want to run a JavaScript application consisting of multiple files, you have to bundle it first with Webpack.

After some investigation, I finally found out that it's possible to use import statements. So your Java application can start a JavaScript application that uses other files. It's just that neither the GraalVM documentation nor most articles tell you how to do that yet. I've uploaded a working demo to my GraalVM demo repository on GitHub.

This is what a simple example looks like:

import org.graalvm.polyglot.*; import org.graalvm.polyglot.proxy.*; public class PolyglotHello { public static void main(String[] args) { System.out.println("This is Java speaking."); var jsCode = "console.log('... and here's JavaScript!');"; try (Context context = Context.create()) { context.eval("js", jsCode); } } }

To execute a JavaScript file, you have to read it first into a string:

var primejs = new String(Files.readAllBytes(Paths.get("prime.js"))); try (Context context = Context.create()) { Value jsBindings = context.getBindings("js"); Value jsProgram = context.eval("js", primejs); ... }

Calling JavaScript methods and passing parameters back and forth

Now the variable jsProgram contains a list of all global variables and methods. We can call the JavaScript method we've already used in the previous installment of this series:

try (Context context = Context.create()) { Value jsBindings = context.getBindings("js"); Value jsProgram = context.eval("js", primejs); Value eratosthenes = jsBindings.getMember("sieve"); Value result = eratosthenes.execute(5_000_000); System.out.println("There are " + result.getArraySize() + " prime numbers below " + 5_000_000); for (var i = 0; i < result.getArraySize(); i++) { System.out.println(result.getArrayElement(i)); } }

Accessing the result is a bit tedious, but it's not a big deal. That's the tribute you pay to abstraction. The API of GraalVM has t work with any JavaScript application, so it uses very generic data types as return values. Everything a JavaScript function returns is a Value. That includes objects, methods, and functions. JavaScript programmers are used to this mental model, but if you've got a Java background, that probably comes as a surprise.

What about performance?

If we're to believe the official figures, the JavaScript performance of GraalVM is a lot better than the performance of the Rhino and Nashorn. We put that to the test. Spoiler: it's true. Mostly, that is.

We converted this example into a proper benchmark, pretty much the same way as we did in the previous part of this series. You can consult the source code at GitHub. The results were encouraging.

1. calculation took 1657 ms 2. calculation took 1326 ms 3. calculation took 937 ms 4. calculation took 542 ms 5. calculation took 526 ms 10. calculation took 421 ms 50. calculation took 237 ms ... 200. calculation took 216 ms The fastest calculation took 214 ms

The performance is slightly worse than the performance of the pure JavaScript application. You know, we expected a performance penalty. The multilingual example sends the result to Java. The node.js example does not. The result is a huge array, consisting of 348513 numbers. So there's little to complain here.

To our surprise, the warm-up phase is less painful in the multilingual program than in the pure JavaScript application. We can only guess. When the Java application starts the JavaScript engine, GraalVM is already up and running, while the pure-JavaScript version starts cold. Whatever the reason is, it's a nice surprise.

Update March 07, 2021:

GraalVM CE 21.0.0 runs the demo a few percent faster. In particular, the start-up penalty has become a lot better.

A string-oriented benchmark

Number-crunching is all good and well, but it's precisely what you're not doing with JavaScript. During our research we stumbled upon a six years old article comparing Rhino and Nashorn, the older Java approaches to run JavaScript. This benchmark uses a JavaScript parser called esprima to tokenize the minified build of jQuery 3.5.1.

Of course, that's a far cry from being a scientific benchmark, but it has several interesting traits:

  • it deals massively with strings
  • it keeps the garbage collector busy
  • it uses non-linear code execution (recursive-descent parsing).

Like always, we've published our benchmark on GitHub, so you run the benchmark yourself.

Benchmark results

On GraalVM 20.1.0, Truffle is the fastest engine, followed by Nashorn and Rhino. To our surprise, it's even faster than native node.js 12.4.1, at least in the long run. There's a severe cold start penalty. It takes GraalVM more than a hundred iterations to overtake node.js. The additional overhead of the Futamura projection shows.

iteration Truffle Nashorn Rhino native node.js
#1 2276 ms 4170 ms 1595 ms 177 ms
#2 872 ms 1220 ms 803 ms 89 ms
#10 398 ms 238 ms 403 ms 59 ms
#20 161 ms 144 ms 366 ms 54 ms
#50 115 ms 107 ms 344 ms 63 ms
#100 63 ms 101 ms 358 ms 50 ms
#150 44 ms 100 ms 344 ms 50 ms
#200 43 ms 99 ms 340 ms 65 ms
#250 42 ms 101 ms 344 ms 65 ms
#300 43 ms 142 ms 354 ms 48 ms

Did I sell Truffle successfully to you? Well, I just compared the performance of the engines on the GraalVM. In the real world, you're probably using one of the good old standard JDKs. So I ran the benchmark again with AdoptOpenJDK 11.0.5. Of course, there's not Truffle. But in the long run, Nashorn almost reaches the performance of both node.js and Truffle.

iteration Truffle Nashorn Rhino native node.js
#1 n/a 5194 ms 1365 ms 177 ms
#2 n/a 1601 ms 650 ms 89 ms
#10 n/a 285 ms 446 ms 59 ms
#20 n/a 216 ms 390 ms 54 ms
#50 n/a 71 ms 400 ms 63 ms
#100 n/a 118 ms 386 ms 50 ms
#150 n/a 64 ms 400 ms 50 ms
#200 n/a 64 ms 388 ms 65 ms
#250 n/a 86 ms 375 ms 65 ms
#300 n/a 65 ms 375 ms 48 ms

We've also run the benchmark with AdoptOpenJDK 14.0.5. For some reason, Java 14 seems to be slower than Java 11. Maybe the LTS versions are heavily optimized, while development on the other version focuses on features. It's not the first time we've observed the performance penalty of Java 14. Newer is not always better. Putting it in IT terms: newer is not always faster.

Doing it the other way round

We've seen it's fairly easy to call a JavaScript function from Java, and we've also seen a pretty good performance, at least in the long run.

Here's the catch. Calling a Java method from a JavaScript (or Ruby, or R, or Python) is more cumbersome.

Come to think of it, that's no surprise except that we'd expect that from a virtual machine that claims to be polyglot. When we heard about it, we thought of a virtual machine that can use any combination of language, each interoperating with every other language.

As it turns out, we're using Truffle, the framework powering the polyglot features, to call methods in foreign languages. Truffle runs a wide variety of programming languages, but there's a huge class of languages not run by Truffle: the languages compiling to bytecode, such as Kotlin, Scala, Ceylon, Groovy, and Java itself.

Update March 07, 2021:

Recently Espresso made in into the official GraalVM release train. Espresso is a Java bytecode interpreter written with Truffle. So if you're ready to sacrifice some performance, you can now easily call Java code from every other language supported by Truffle. Currently, the Espresso team is working hard on improving the performance of Espresso, but it's unlikely it'll ever match the performance of the native Java implementation. On the other hand, if the primary language of your application is not Java, the performance of Espresso isn't important. Espresso opens opportunities you didn't have without it.

Cutting a long story short: Truffle enables a remarkable amount of interoperability, but it's not perfect. Java (run the tradional way, i.e. without Espresso) and the JVM languages get a special treatment in Truffle. Any Java object you want to use must be passed to Truffle. To do so, just call the method putMember() of the Truffle Context:

public static class MyClass { public int id = 42; public String text = "42"; public int[] arr = new int[]{1, 42, 3}; public Callable ret42 = () -> 42; } public static void main(String[] args) { try (Context context = Context.newBuilder() .allowAllAccess(true) .build()) { context.getBindings("js") .putMember("javaObj", new MyClass()); boolean valid = context.eval("js", " javaObj.id == 42" + " && javaObj.text == '42'" + " && javaObj.arr[1] == 42" + " && javaObj.ret42() == 42") .asBoolean(); assert valid == true; } }

Other programming languages

The API isn't limited to Java and JavaScript. Every language registering to Truffle can be called from every Truffle language. That includes Ruby, Python, R, and all the LLVM language, the most important of them being C/C++.

The nice thing about it is that not only Java can call code written in any of these languages, but these languages can also call each other. You Ruby program can call R, which calls a C function. Nice!

Limits of interoperability

There are several limits to interoperability. The GraalVM project documentation lists some of them. We've already mentioned data types. The Truffle API allows for many data types, but Truffle is written in Java, so the list of data types remains a bit Java-centric. If your favorite language has a fancy data type that's not supported by Java, you probably have to convert it to a more basic data type first. Thread management and using thread-local variables from JavaScript isn't supported by Truffle, either.

Wrapping it up

About the co-author

Karine Vardanyan occupies herself with making her master at the technical university Darmstadt, Germany. Until recently, she used to work at OPITZ CONSULTING, where she met Stephan. She's interested in artificial intelligence, chatbots, and all things Java.
Polyglot programming has been around in the Java universe for some time. The GraalVM take it to another level. You're still slightly limited by the API approach, which makes your guest language a second-class citizen in your application. You have to know where the source code is, and you have to load it manually, either using the file API or by using the Source class of the GraalVM. Other than that, there's little to complain. The API is simple and does the trick.

We've even seen a decent performance in our tests. Not perfect, but we're sure future GraalVM versions have a lot in store for us. So what can you do with the polyglot approach?

Among other things, you can use Truffle to store algorithms in the database. Please proceed extremely carefully, because that's the hacker's dream. Nonetheless, almost every application can be configured. Configuration is the key to success. Being able to configure algorithms may give your company an extra sales boost.

Another benefit is you can use algorithms that happen to be written in the wrong language. Now you don't have to migrate them. You can use LLVM to cross-compile them and run them using Truffle.

We're curious how people are going to use this feature to good use!

It goes without saying we're less curious to see how evil hackers are going to use this feature, so be careful if you're exposing your application to algorithms stored in the database. It's a two-sided sword.


Dig deeper

Calling out from Java to JavaScript (with call back)

Introduction to Nashorn

Rhino

Our number-crunching benchmark on GitHub

Our string-oriented benchmark on GitHub


Comments