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.
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
- High-level introduction to the GraalVM series
- Low-level stuff: bytecode, interpreters, and compilers
- Optimization strategies of the GraalVM
- Tree rewriting: how to implement an optimizer?
- Hands-on experience with GraalVM 2019.3. Is it ready yet?
- Polyglot programming. Including JS and Ruby benchmarks.
- Multilingual programming: using the best of many worlds
- Truffle - Graal's compiler-compiler. Polyglot programming under the hood.
- Unleashing the power of native cloud computing
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:
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.
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
:
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.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)