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.
- 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
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
.
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
.
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.
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.