JAX Blog

GraalVM vs. JVM:

Neue polyglotte Programmierung mit dem Truffle API

Jan 13, 2020

Viele, die im Java-Umfeld unterwegs sind, werden von ihr gehört haben: der sagenumwobenen GraalVM. Diese „magische“ neue Virtual Machine für Java soll vor allem für blanke Performance sorgen, indem sie den Java-Bytecode in nativen Code kompiliert.

Viele, die im Java-Umfeld unterwegs sind, werden von ihr gehört haben: der sagenumwobenen GraalVM. Diese „magische“ neue Virtual Machine für Java soll vor allem für blanke Performance sorgen, indem sie den Java-Bytecode in nativen Code kompiliert. Dadurch fällt insbesondere der Start-up-Overhead weg, da weite Teile der Initialisierung bereits vom Compiler erledigt werden. So oder so ähnlich ist es vielerorts zu lesen[1], [2]. Doch das ist bei weitem nicht das einzige Feature, das Oracle der GraalVM gegeben hat. Hinzu kommt, dass sie zu nichts weniger das Potenzial hat, als eine neue Ära der polyglotten Programmierung auf der JVM einzuläuten. Die Rede ist vom Truffle API, einem generischen Framework zur Implementierung von Interpretern.

Als die JVM 1994 neu herauskam, war sie noch untrennbar mit Java, der Sprache, verbunden. Deutlich zu erkennen ist, dass der JVM-Bytecode so angelegt worden war, dass sich Javas „Geschmacksrichtung“ der objektorientieren Programmierung gut darin abbilden ließ. Das ist aus historischer Sicht konsequent, denn schließlich sollte die JVM den kompilierten Java-Code effizient ausführen können.

Über die Jahre hinweg bekamen sowohl die JVM als auch Java neue Features, Optimierungen und Verbesserungen. Klar war, dass Sun Rückwärtskompatibilität als höchstes Gut auffasste. So sollten einmal kompilierte Java-Programme praktisch unbegrenzt in zukünftigen JVM-Versionen lauffähig bleiben.

Die Krönung der Rückwärtskompatibilität erfolgte dann knapp zehn Jahre später mit der Veröffentlichung von Java 5. Im Java-Compiler wurde das Typsystem durch die Einführung von Generics kräftig umgekrempelt. Doch am Bytecode änderte sich recht wenig: Generische Typen werden vom Compiler kurzerhand entfernt, um Kompatibilität mit existierendem Code nicht zu gefährden. Man spricht von Type Erasure.

Anders getypte und ungetypte Sprachen

Parallel zu diesen Entwicklungen haben kluge Köpfe an alternativen Sprachdesigns gearbeitet, die mehr oder weniger an Java angelehnt sind.
Bei Scala, das ungefähr zeitgleich mit Java 5 erschien, hat sein Erfinder Martin Odersky das Grundkonzept der Objektorientierung beibehalten, alte Zöpfe abgeschnitten und gleichzeitig funktionale Programmierung auf der JVM salonfähig gemacht. Dank Type Erasure konnte Odersky sich im Typsystem austoben und musste nur wenig Rücksicht auf die Generics in Java 5 geben. Scalas fortgeschrittenes Typsystem spielt so in dem generierten Bytecode gar keine Rolle mehr.

Ungefähr ein Jahr zuvor war bereits die Sprache Groovy erschienen, die ursprünglich als standardisierte Skriptsprache auf der JVM antreten wollte. Aus dem zugehörigen JSR 241 wurde jedoch nichts und Groovy entwickelte sich zu einer unabhängigen Programmiersprache. Im Gegensatz zu Java verzichtet Groovy an vielen Stellen auf Typen, sodass Methoden häufiger per Reflection aufgerufen werden als in Java. Erst zur Laufzeit wird klar, in welcher Klasse die jeweilige Methode implementiert ist. Für einen Aufruf ohne Reflection müsste die konkrete Methode aber schon feststehen, wenn der Bytecode generiert wird. Also muss man den Reflection Overhead hinnehmen.

Ist die JVM groß genug für mehrere Sprachen?

Scala und Groovy sind bei weitem nicht die einzigen Beispiele für alternative JVM-Sprachen. Manche sind typisiert, andere eher dynamisch. Allen ist jedoch gemeinsam, dass die JVM wegen ihrer Performance und bestehender Bibliotheken eine für sie attraktive Plattform darstellt. Auch Portierungen existierender Sprachen (Jython, JRuby) wurden in den 2000er-Jahren zunehmend populär.

2011 folgte dann Java 7, das die lang erhoffte Bytecodeerweiterung invokedynamic brachte. Damit lassen sich sehr effizient dynamische Methodenaufrufe implementieren, die in ungetypten Sprachen dominieren. Vorangegangen war die Arbeit an der sogenannten Da Vinci Virtual Machine [3], mit der Sun an einem First-Class-Support für andere Sprachen auf der JVM experimentiert hat.

In Java selbst werden nicht statische Methodenaufrufe mit den Instruktionen invokevirtual oder invokeinterface durchgeführt. Dabei sind die Argumenttypen bereits festgelegt, aber die implementierende Klasse wird zur Laufzeit ausgewählt. Im einfachsten Beispiel:

class A { int f() { /* ... */ } }
class B extends A { @Override int f() { /* ... */ }}

Je nachdem, welcher Klasse ein Objekt x angehört, wird beim Aufruf x.f() entweder A.f() oder B.f() ausgeführt.
In ungetypten Sprachen hingegen müssen Methoden regelmäßig auch nach ihren Argumenten ausgewählt werden, um z. B. zwischen der Addition von Zahlen oder von Strings zu unterscheiden. Dazu dient invokedynamic, wozu die Oracle-Dokumentation [4] Folgendes erläutert: „[It] enables the runtime system to customize the linkage between a call site and a method implementation.“

Implementierungsaufwand

Ambitionierten Entwickler*innen, die ihrer Sprache ein JVM Backend verpassen wollen, steht eigentlich nichts im Wege. Wäre da nicht das Problem des Compilerbaus. Oftmals werden daher Interpreter statt Compiler für Sprachen gebaut, denn dann kann man die Laufzeitinfrastruktur der Hostumgebung einfach direkt benutzen.

Im Gegensatz dazu ist es sehr schwierig, maßgeschneiderten, korrekten JVM-Bytecode zu produzieren. Die invokedynamic-Instruktion ist ein Paradebeispiel hierfür: Jeder derartige Aufruf erfordert eine Bootstrap-Methode, die den korrekten Method Handle – ebenfalls eine Neuerung in Java 7 – bereitstellt. Diese Handles lassen sich zwar viel effizienter aufrufen als die üblichen Reflection Methods, haben aber ein komplexeres API. Eine Einführung in dieses API gibt es z. B. in diesem Artikel [5].

Doch nur mit einem „echten“ Compiler kann man effektive Programmoptimierungen vornehmen. Ein Interpreter auf Basis der JVM hat zudem immer das Problem, dass man zwei Interpreterschichten aufeinander hat (JVM-Bytecode durch die JVM, Sprachquelltext durch den Sprachinterpreter). Dass das der Performance nicht sonderlich zuträglich ist, überrascht nicht.

Futamuras Vision

Wie wäre es also, wenn es ein System gäbe, das einen Sprachinterpreter vollautomatisch in einen Sprachcompiler transformiert? Also sozusagen einen Compiler-Compiler?
Dieses futuristisch anmutende Konzept wurde bereits 1971 von Yoshihiko Futamura beschrieben und trägt daher passenderweise den Namen Futamura Projection.
Die Futamura-Projektionen, auch unter partieller Auswertung bekannt, bezeichnen ein Bündel von Technologien, mit dem man ein gegebenes Programm für bestimmte Eingaben spezialisieren kann. Angenommen, eine Funktion erwartet zwei Eingabewerte. Der erste Eingabewert bestimmt den Programmfluss, z. B. durch eine Kaskade von if-else-Anweisungen. Eine mögliche Optimierung wäre hier, für alle bekannten Möglichkeiten dieses Eingabewerts (z. B., wenn es sich um ein enum handelt) spezialisierte Funktionen zu generieren, die keine weiteren Bedingungen enthalten. Bis zu einem bestimmten Grad können bestehende JVMs das bereits, wobei sie z. B. virtuelle Methodenaufrufe optimieren können, wenn klar ist, welche konkrete Methode angesprungen wird.

Scala hat eine ähnliche Optimierung in den Compiler eingebaut. Die Sprache erlaubt es nämlich, Generics auch mit primitiven Typen zu instanziieren, wobei der Compiler automatisch Boxing und Unboxing betreibt. Wird der Typparameter einer Methode oder Klasse mit @specialized annotiert, so werden zur Compile-Zeit Varianten erzeugt, die direkt mit nativen Typen arbeiten. Dadurch kann man sowohl Speicher- (keine Objektallokation) als auch Laufzeit-Overhead (keine Konvertierungen) einsparen.

Trotzdem sind solche Optimierungen bestenfalls punktuell. Eine vollwertige Futamura-Projektion ist hingegen allgemein und erzeugt ein sogenanntes Residual Program, sprich ein Programm, bei dem der gesamte Kontrollfluss, der bereits zur Compile-Zeit bekannt ist, ausgeneriert ist. Man kann sich das – angewendet auf Interpreter – wie folgt vorstellen. Ein mögliches Interface für einen Interpreter kann man in etwa so definieren:

interface Interpreter {
    Object runCode(String code, Object input);
}

Dieser Interpreter führt zwei verschiedene Logiken aus:

  1. Interpreterlogik (gegeben durch die Implementierung von runCode)
  2. Programmlogik (gegeben durch den Programmtext code; um genau zu sein, handelt es sich hier nicht um den Quelltext, sondern um einen Syntaxbaum.)

Die erste Futamura-Projektion verlangt, auf Basis eines gegebenen Programmtextes die runCode-Methode dergestalt zu optimieren, dass keine Interpreterlogik mehr vorhanden ist, sondern nur noch Programmlogik. Es gibt noch zwei weitere Projektionen, die aber für diesen Artikel nicht relevant sind.

Viele Jahrzehnte nach der erstmaligen Beschreibung dieses Konzepts ist es Oracle mit GraalVM gelungen, eine produktionsreife Implementierung der ersten Futamura-Projektion vorzulegen: Man schreibe einen Interpreter – zum Beispiel für JavaScript (JS) – in Java und erhält automatisch einen JIT-optimierenden Compiler, der die bisherigen spezialisierten Interpreter Rhino und Nashorn schlägt [6].

Bleiben wir doch bei JS als Beispiel einer Sprache, die mittlerweile universell eingesetzt wird und für die es gute Gründe gibt, sie in die JVM einbetten zu wollen. Mit Hilfe des Truffle API, das mit GraalVM ausgeliefert wird, lässt sich z. B. Multiplikation in JavaScript implementieren, wie in Listing 1 – stark gekürzt – gezeigt.

Listing 1

public abstract class JSMultiplyNode extends JSBinaryNode {

  public abstract Object execute(Object a, Object b);

  @Specialization(guards = "b > 0", rewriteOn = ArithmeticException.class)
  protected int doIntBLargerZero(int a, int b) {
    return Math.multiplyExact(a, b);
  }

  @Specialization(rewriteOn = ArithmeticException.class)
  protected int doInt(int a, int b) {
    // ...
  }

  @Specialization
  protected double doDouble(double a, double b) {
    return a * b;
  }

  // ...
}

Durch einfache Annotationen wie @Specialization wird Truffle signalisiert, welche Methoden für spezielle Objekttypen aufgerufen werden sollen. Im Allgemeinen kann in JavaScript erstmal alles mit jedem multipliziert werden, aber für die Fälle int, int und double, double werden hier effiziente Implementierungen vorgegeben.
Dank des Truffle API kann man systematisch einen Interpreter implementieren, der von der GraalVM automatisch als JIT-Compiler benutzt wird. Performanceoptimierungen werden, wie oben annotiert, dabei automatisch berücksichtigt.

GraalVM vs. JVM

An dieser Stelle sollten wir einmal genau unter die Lupe nehmen, in welcher Beziehung Truffle zum Ökosystem steht. Dazu müssen wir einige Begrifflichkeiten klären.
Unter der GraalVM versteht man sowohl das gesamte Projekt unter Federführung von Oracle als auch eine (derzeit) Java-8-kompatible Implementierung einer Java Virtual Machine. Damit reiht sich die GraalVM in eine Reihe anderer Implementierungen ein, z. B. Azul, und existiert neben HotSpot, der am weitesten verbreiteten JVM von Oracle, die mit dem OpenJDK ausgeliefert wird.
Code, der auf der GraalVM läuft, wird weiterhin wie üblich als Bytecode interpretiert. Für die Umwandlung in nativen Code ist ein sogenannter Ahead Of Time (AOT) Compiler zuständig. Im Gegensatz zum bekannten Just-in-time-(JIT-)Compiler wird Bytecode damit bereits vor der Ausführung in Maschinencode übersetzt. Der AOT-Compiler der GraalVM erzeugt ein Native
Image, das eine minimale Laufzeitinfrastruktur beinhaltet (SubstrateVM). AOT-Kompilierung wird ebenfalls in der Android Runtime (ART) benutzt.

Die Implementierung von alternativen Sprachen, wie z. B. JavaScript, erfolgt mit dem Truffle Framework, das sowohl auf der GraalVM als auch der SubstrateVM lauffähig ist. Darüber hinaus können manche mit Truffle implementierten Sprachen (aber nicht alle) auch auf der HotSpot-VM (oder anderen JVMs) ausgeführt werden, wobei in diesem Fall die Performance niedriger ist.
Die nahtlose Einbettung solcher Sprachen in Java-Programme erfolgt durch das Polyglot API, das über Sprachen abstrahiert und den Austausch von Objekten ermöglicht. Wie im oberen Codeschnipsel zu sehen ist, werden intern die gewohnten Java-Typen verwendet. Umgekehrt kann man auch sehr einfach Java-Code aus JS-Code aufrufen. Trotzdem erlaubt es die GraalVM, den Guest-Code isoliert laufen zu lassen (separater Heap und Garbage Collection) und die Menge der zulässigen Operationen feinziseliert festzulegen.

Eine neue Ära?

JavaScript ist derzeit eine der bestunterstützten Sprachen in der GraalVM. Für diejenigen, die sich aber weniger für die coole Technologie im Hintergrund interessieren, sondern sich fragen, welche konkreten Vorteile die GraalVM in der Praxis bringt, haben die Entwickler*innen von Oracle vorgesorgt.
Zum einen bringt die GraalVM-Distribution eine komplette Node.js-Umgebung mit, die auch das Kommandozeilentool node beinhaltet. Das bedeutet, dass sich die meisten üblichen npm-Pakete auch nahtlos in einer Java-Umgebung ausführen lassen. Damit hat GraalVM einen haushohen Vorteil sowohl gegenüber Rhino als auch gegenüber Nashorn, die viele neuere Sprachkonstrukte aus den Revisionen ES6 und später nicht unterstützen.

Zum anderen lässt sich eine polyglotte Anwendung nahtlos in nativen Code übersetzen. Damit hat Oracle einen hohen Grad an Orthogonalität ihrer Features erreicht.
So wird heute schon Realität, wovon man früher nur träumen konnte: Gemischte JS-/JVM-Projekte können mit wenig Aufwand gebaut und effizient ausgeführt werden. Nutzt man z. B. Gradle als Build-Tool, gibt es dafür ein Node.js-Plug-in [7], mit dem sich npm-Pakete installieren und bauen lassen. Die so paketierten JS-Files lassen sich dann direkt in die JVM laden und ausführen.

Neben JavaScript gibt es aber noch andere unterstützte Sprachen:

  • Sofern man an statistischen Berechnungen interessiert ist, bindet man kurzerhand R ein.
  • Dank Python-3-Support lässt sich im Gegensatz zu Jython moderner Python-Code ausführen.
    Ruby-Applikationen können parallelisiert werden, da die Einschränkung durch den Global Interpreter Lock (GIL) wegfällt.

Auch für Menschen, die von Java nichts halten, könnte sich ein Blick auf die GraalVM lohnen: Selbstverständlich lässt sich das Polyglot API auch aus JavaScript, Ruby oder Python heraus nutzen, weshalb man auch diese Sprachen miteinander mischen kann. Faszinierenderweise implementiert GraalVM das Chrome DevTools Protocol [8], womit sich normalerweise JavaScript im Browser debuggen lässt, dank Polyglot API gilt das aber auch für andere Sprachen. Damit lassen sich in Chrome Breakpoints setzen, z. B. in Ruby-Code, der einen JavaScript-Webserver startet.
Das Truffle API ist aber auch ausgezeichnet dafür geeignet, domänenspezifische Sprachen zu designen. Wie das geht, wird in einem Beispiel-Repository demonstriert [9].

Nativer Code? In meiner JVM?

Die oben genannten Sprachen werden gewöhnlich immer in einer VM beziehungsweise einem Interpreter ausgeführt. Daher ist es nicht so überraschend, dass man sie auch auf einer JVM implementieren kann. GraalVM kann allerdings auch LLVM Bitcode – eine Art Assemblersprache – ausführen. Dadurch kann man performance- oder speicherkritischen Code in systemnahen Sprachen wie Rust implementieren und Seite an Seite mit Java-Code laufen lassen.
Es lohnt sich, diese Interoperabilität genauer unter die Lupe zu nehmen. Wenn man bisher nativen Code in der JVM laufen lassen wollte, hatte man im Wesentlichen zwei Optionen:

  1. Java Native Interface (JNI), offizieller Bestandteil der Java-Spezifikation
  2. Java Native Access (JNA), als Community-Bibliothek bereitgestellt

Unterschiede zwischen den beiden Ansätzen zeigen sich z. B. daran, dass es in JNA nicht vorgesehen ist, Java-Objekte direkt aus C/C++ zu manipulieren oder Methoden aufzurufen. Im Gegensatz dazu braucht nativer Code für die Benutzung in JNA nicht mit speziellen Headerfiles kompiliert zu werden. Daneben gibt es noch zahlreiche andere Unterschiede, z. B. in der Performance.

Beiden Ansätzen ist allerdings gemein, dass für Java-Code, der nativen Code aufrufen möchte, eine plattformspezifische native Bibliothek geladen werden muss. Eine solche muss im Regelfall binärkompatibel zu C sein. Wenn es sich dabei nicht um die Standard-C-Bibliothek handelt, sind entweder User dafür verantwortlich, sie nachzuinstallieren, oder die Java-Bibliothek muss sie mitliefern. Im schlimmsten Fall müssen dabei Varianten für Windows, Linux, macOS, BSD und weitere berücksichtigt werden. Ferner führen Crashes in nativem Code automatisch zum Crash der gesamten JVM.

GraalVM verbessert diese Situation deutlich. Statt nativen Code direkt einzubinden, geht man den Umweg über den sogenannten Bitcode des LLVM-Projekts. Ähnlich wie GraalVM ist LLVM ein Infrastrukturprojekt, das unter anderem von Apple und Google vorangetrieben wird. Unter deren Dach wird ein moderner C/C++-Compiler (Clang) entwickelt, sowie ein generisches, optimierendes Backend, das andere Compiler zur Codegenerierung benutzen können. Das Rust-Projekt nutzt zu diesem Zweck LLVM. In gewisser Weise kann man sich also LLVM-Bitcode als Konkurrenten zu JVM-Bytecode vorstellen. Im Gegensatz zu JVM-Bytecode wird LLVM-Bitcode aber im Regelfall vor der Ausführung in nativen Code (AOT) transformiert. GraalVM hingegen kann den Bitcode ausführen und damit in die Polyglot-Infrastruktur einbinden.

Darüber hinaus bietet die Enterprise Edition von GraalVM auch einen Sandbox-Modus für C-Programme. In diesem Modus werden sämtliche Speicherzugriffe und Systemaufrufe umgebogen, sodass dieselben Zugriffsbeschränkungen wie z. B. bei JavaScript konfiguriert werden können. Insbesondere reißen Crashes nicht die ganze Applikation mit sich, sondern äußern sich in einer Exception. Beispiel gefällig? Betrachten wir folgenden C-Code (adaptiert von einem Blogeintrag des Graal-Teams [10]):

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("number of arguments: %n\n", argc);
  return 0;
}

Der Fehler ist, dass der Format-Modifier %n die Zahl der bisher geschriebenen Bytes in das Argument speichert, aber argc kein Zeiger ist. Gängige Compiler warnen zwar vor diesem Problem, kompilieren das Programm aber trotzdem.
Mit dem Java-Code in Listing 2 kann man den Bitcode laden und ausführen:

Listing 2

class Sandbox {
  public static void main(String[] args) throws IOException {
    Source source =
      Source.newBuilder("llvm", new File("bug.bc")).build();

    Context.Builder builder =
      Context.newBuilder()
        .allowIO(true)
        .option("llvm.sandboxed", "true");
    try (Context polyglot = builder.build()) {
      polyglot.eval(source).execute();
    }
    catch (Exception ex) {
      System.out.println("something went wrong: " + ex);
    }
  }
}

Zur Laufzeit erhält man diese Exception:

number of arguments: something went wrong: org.graalvm.polyglot.PolyglotException: Illegal pointer access: 0x0000000000000001

Normalerweise würde das reine C-Programm aber abbrechen.

Rusten statt rosten

Dank der Sandbox lassen sich auch fehlerhafte C-Programme sicher ausführen. Wer aber die Enterprise Edition nicht nutzen kann oder möchte, muss stattdessen auf eine sichere Programmiersprache zurückgreifen. Dafür bietet sich Rust an, da der Compiler ebenfalls LLVM-Bitcode erzeugen kann.

Die Herausforderung dabei ist, eine robuste Integration zwischen der objektorientierten Java-Welt und der eher systemnahen Rust-Welt zu schaffen. Diese Integration basiert darauf, dass Java- und Rust-Funktionen sich gegenseitig aufrufen und Objekte austauschen können.
Um das zu verstehen, ist die Struktur des LLVM-Bitcodes wichtig. Diese ähnelt nämlich der Stuktur des JVM-Bytecodes: Es wird eine Reihe von Funktionen (optional mit Parametern und Rückgabewert) definiert. Die Funktionskörper bestehen dann aus einer Reihe von Instruktionen, z. B. arithmetischen Routinen oder Aufrufen von anderen Funktionen. LLVM verlangt (genau wie die JVM), dass der Bitcode typkorrekt ist.

Eine weitere Gemeinsamkeit ist, dass der Bitcode unvollständig sein kann. Das bedeutet: Manche Funktionen werden nur deklariert, d. h., sie müssen an einer anderen Stelle definiert sein, damit ein Aufruf erfolgreich ist. In Java ist das der Fall, wenn eine Methode einer anderen Klasse aufgerufen wird. Die JVM lädt Klassen lazily, d. h., wenn ein Aufruf erfolgt, wird im Classpath nach der passenden Klasse gesucht.

Bei LLVM bzw. nativem Code ist das etwas komplizierter. Normalerweise müsste man sich auf das dynamische Linken des Betriebssystems verlassen. Im Kontext von Graal ist das nicht möglich, da die JVM nicht beliebige Bibliotheken nachladen kann (GraalVM bietet eine Kommandozeilenoption, mit der eine Liste von nativen Bibliotheken bereits zum Startzeitpunkt geladen werden kann, das aber nur plattformabhängig). Stattdessen muss man durch geeignete Compiler-Flags den Rust-Compiler anweisen, statisch gelinkte Bitcode-Dateien zu erzeugen. Das funktioniert aber nur soweit, wie alle benutzten Abhängigkeiten selbst wiederum in Rust implementiert sind und daher auch als LLVM-Bitcode vorliegen. Systemnaher Code ruft aber eben auch manchmal Routinen des Betriebssystems auf. GraalVM kann solche Calls durchführen, indem es sie 1:1 weiterreicht. Die Enterprise Edition kann im Sandboxing-Modus eine bestimmte Liste von Calls auch abfangen und umleiten.

Ruft nun Java-Code eine Funktion aus Rust auf, wird diese im geladenen Bitcode per Namen gesucht, denn Überladung ist in Rust nicht möglich. Der umgekehrte Weg ist nicht so einfach. Zunächst bietet die Installation der GraalVM eine C-Header-Datei an, in der bestimmte Funktionen deklariert sind, mit denen sich Java-Klassen laden, instanziieren und ihre Methoden selektieren und aufrufen lassen. Diese C-Funktionen werden in der GraalVM implementiert und liegen daher nicht im Bitcode vor. Während der Ausführung von Rust-Code werden sie in entsprechende Java-Reflection-Aufrufe umgewandelt. Bekannte Typen wie Integer werden automatisch konvertiert.
Typischer Code, mit dem sich Java-Objekte erzeugen und manipulieren lassen, sieht aus wie in Listing 3.

Listing 3

unsafe {
  let buf = polyglot_new_instance(polyglot_java_type("java.lang.StringBuffer\0"));
  let value = polyglot_from_string_n(str, str.len(), "UTF-8\0".as_ptr());
  polyglot_invoke(buf, "append\0".as_ptr(), value);
  // ...
}

Bei aller Interoperabilität bleibt eine Einschränkung: Es ist nicht möglich, Zeiger auf Java-Objekte außerhalb des lokalen Stacks einer Rust-Funktion zu verwalten. Insbesondere kann man sie nicht in komplexere Datenstrukturen verpacken. Das liegt daran, dass sich diese Zeiger von GraalVM nicht wie gewöhnliche Zeiger verhalten; daher lassen sich Arrays auch nur schwer und Zeigerarithmetik sogar unmöglich abbilden.

Fazit

Es lohnt sich, die GraalVM ganz genau anzuschauen, denn sie hat viele Fähigkeiten, die radikal neu sind. Die Entwicklung schreitet rasch voran: Während derzeit zwar die offizielle GraalVM nur kompatibel zu Java 8 ist, wird schon unter Hochdruck an Releases für neuere Java-Versionen gearbeitet. Die polyglotte Programmierung ist aber unbeschadet davon auch weitestgehend mit neueren JVMs möglich, wenn man auf die Erzeugung von nativem Code verzichten kann. Gut möglich, dass wir dank der Kombination dieser Features demnächst wieder mehr Java auf dem Desktop sehen.

Links & Literatur

  • [1] Fischer, Oliver B.: „Eine Einführung in GraalVM, Oracles neue Virtual Machine. Eine, um alles zu beherrschen“, in: Java Magazin 5.2019
  • [2] https://www.heise.de/developer/artikel/Die-GraalVM-Javas-Sprung-in-die-Gegenwart-4467307.html
  • [3] https://openjdk.java.net/projects/mlvm/
  • [4] https://docs.oracle.com/javase/8/docs/technotes/guides/vm/multiple-language-support.html
  • [5] Heiden, Markus: „Bytecode 2.0. Was ist invokedynamic, und warum ist es wichtig“, in: Java Magazin 11.2012
  • [6] https://medium.com/graalvm/oracle-graalvm-announces-support-for-nashorn-migration-c04810d75c1f
  • [7] https://github.com/srs/gradle-node-plugin/blob/master/docs/node.md
  • [8] https://www.graalvm.org/docs/reference-manual/tools/#debugger
  • [9] https://github.com/graalvm/simplelanguage
  • [10] https://medium.com/graalvm/safe-and-sandboxed-execution-of-native-code-f6096b35c360

 

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

Alle News der Java-Welt:

Behind the Tracks

Agile, People & Culture
Teamwork & Methoden

Clouds & Kubernetes
Alles rund um Cloud

Core Java & Languages
Ausblicke & Best Practices

Data & Machine Learning
Speicherung, Processing & mehr

DevOps & CI/CD
Deployment, Docker & mehr

Microservices
Strukturen & Frameworks

Performance & Security
Sichere Webanwendungen

Serverside Java
Spring, JDK & mehr

Software-Architektur
Best Practices

Web & JavaScript
JS & Webtechnologien

Digital Transformation & Innovation
Technologien & Vorgehensweisen

Domain-driven Design
Grundlagen und Ausblick

Spring Ecosystem
Wissen in Spring-Technologien

Web-APIs
API-Technologie, Design und Management