Skip to content

Latest commit

 

History

History
 
 

espresso-jshell

GraalVM Demos: Native jshell and Espresso

This demo showcases the integration between GraalVM Native Image and Java-on-Truffle (Espresso).
It builds a native image of jshell, that executes the dynamically generated bytecodes on Espresso. This hybrid mode achieves instant startup, beating the vanilla jshell in both: time to the first interaction and time to evaluate a simple expression.

JShell is a Java read-eval-print loop tool first introduced in Java 9, this demo also allows running jshell on Java 8.
For further discussions and questions please join our #espresso channel on the GraalVM Slack Community.

Prerequisites

  • GraalVM for Java 11, 17 or higher
  • Native Image support
  • Java-on-Truffle (Espresso) support

Download the latest GraalVM here.
Having GraalVM installed, install the Native Image and Java on Truffle (Espresso) components.
GraalVM bundles gu, a command line utility to install and manage additional functionalities/components; to install the Native Image and Java on Truffle (Espresso) components, run the following command:

<graalvm>/bin/gu install native-image espresso

How to Build

Set the GRAALVM_HOME environment variable to the GraalVM home:

export GRAALVM_HOME="/path/to/graalvm"

Then execute the build-espresso-jshell.sh script:

./build-espresso-jshell.sh

It generates a native executable: espresso-jshell in the working directory. This native executable, espresso-jshell [options...], runs with almost-instant startup.

Launch ./espresso-jshell -Dorg.graalvm.home="$GRAALVM_HOME", execute some Java code and see the output immediately. To exit the shell, type /exit.

Running jshell on Java 8

jshell was first introduced in Java 9, but thanks to Java's excellent backwards compatibility, it's possible to run jshell on a Java 8 environment.

export GRAALVM_HOME="/path/to/graalvm"
export JDK8_HOME="/path/to/jdk8"
./jshell8.sh [options...]

It may seem that espresso-jshell is running Java 11 in this mode, and it is.
jshell (the frontend) is compiled by native-image with Java 11 (or 17), but the Java compiler (javac has a Java API used by jshell) is fully backwards compatible with Java 8 e.g. javac -source 8 -target 8 -bootclasspath JAVA8_BOOT_CLASSPATH.
The generated bytecodes are then executed in Espresso (the backend) which runs a Java 8 guest JVM.

You can run the following snippet to convince yourself that it indeed, runs on Java 8:

System.getProperty("java.vm.version");

// `jshell -C-source -C8` forbids getModule() access from code,
// but it is still accessible through reflection.
Object.class.getModule();

// In true Java 8 mode, getModule() is not accessible at all,
// not even through reflection.
Class.class.getDeclaredMethod("getModule");

Pass system properties to Espresso with -R-Dkey=value.
Pass polyglot options to Espresso with -Rjava.InlineFieldAccessors -Rengine.Compilation=false.

How it works?

For hybrid projects like this one, a clear boundary between host and guest code is a requirement.
jshell has two main components: the frontend, which includes the console interface and the Java compiler; and the backend, referred as the "execution engine", where the dynamically generated bytecodes are executed.

jshell allows to implement custom backends, see jdk.jshell.spi.ExecutionControl. This is a very clean boundary since there are no complex objects crossing it, only primitives, strings, arrays and a few exceptions. It also provides a few implementations e.g. JdiDefaultExecutionControl which spawns another process and communicates with it through JDI, LocalExecutionControl which runs in the same VM as jshell.

To communicate between host and guest worlds, an adapter was implemented in host Java that, via interop, forwards all methods calls to an Espresso guest object, taking care of converting all the arguments and return values, translating guest exceptions to host exceptions...
On the Espresso side, a guest LocalExecutionControl executes all the methods calls forwarded by the host adapter; any other guest ExecutionControl implementation could be used.

espresso-jshell runs partially on a guest LocalExecutionControl instance, so it behaves similar to jshell -execution local and shall not be compared with the default jshell execution mode e.g. class redefinition is not supported in this mode, jshell -execution local does not support it either. It does not support other execution engines other than -execution espresso, which is our host-to-guest adapter, see EspressoLocalExecutionControlProvider.

Since the Java compiler is part of the host code compiled by native-image, dynamically loaded annotation processors are not supported. Annotation processors must be compiled-in AOT by native-image. But this limitation could be lifted: since the Java compiler provides an interface to implement annotation processors, it could be possible to run the Java compiler on the host and the dynamically loaded annotation processors on Espresso using the same idea we just described.

Distribution

espresso-jshell is not fully standalone, it doesn't bundle jars/jmods nor the core Java native libraries. jshell also needs a java.home for the host Java compiler. Specifying a GraalVM home is the easiest way for Espresso and jshell to find all these dependencies e.g. ./espresso-jshell -Dorg.graalvm.home="$GRAALVM_HOME".
espresso-jshell doesn't require a full-blown GraalVM distribution to run. It can run with a minimal/jlink-ed Java home that includes the Espresso home ($GRAALVM_HOME/languages/java), mimicking the same folder structure as GraalVM.
I successfully ran espresso-jshell on a minimal Java home (11) with only java.base + Espresso home, weighting just ~26MB uncompressed, ~9.8MB zipped; in addition to the espresso-jshell executable.