Javascript

De laatste jaren maakt Javascript een snelle ontwikkeling door. De taal wordt steeds vaker buiten de context van een browser gebruikt. NodeJs, een framework waar je server-side en network applicaties mee kunt bouwen, is daarvan wellicht het bekendste voorbeeld. Ook wordt JavaScript steeds belangrijker als generieke scripttaal. Oracle haakt hierop in met Project Nashorn (Duits voor neushoorn). Dit is een ‘lightweight’, ‘native’ en ‘high performance’ implementatie van een JavaScript-engine, die bij JDK 8 wordt gedistribueerd. Dit vervangt de vorige versie (Rhino), die sinds Java 6 SE wordt meegeleverd. Rhino gebruikt veel resources en is relatief langzaam in vergelijking met Nashorn.

Net als Rhino is Nashorn een standalone scriptingtaal en heeft hij geen Browser Plugin API of ondersteuning voor het Document Object Model (DOM). Het is dus niet mogelijk om een DOM-framework te gebruiken, zoals jQuery, Dojo of Prototype. Wel biedt Nashorn ondersteuning voor JavaFX en commandline scripts, die je in een batch kunt gebruiken. Een belangrijk verschil tussen Nashorn en Rhino is dat Nashorn alleen ECMA-Script versie 5 implementeert. Rhino biedt aanvullende functionaliteit, zoals enkele constructies, die zijn geïntroduceerd in JavaScript 1.7. Verder zijn de verschillen relatief klein en lijken Nashorn en Rhino qua functionaliteit op elkaar.

De broncode van Rhino is niet opnieuw gebruikt. Nashorn is vanaf ‘scratch’ herschreven. Dit is waarschijnlijk gedaan om niet gehinderd te worden door de legacy van Rhino en om optimaal gebruik te maken van de Java Virtual Machine (JVM) versie 7, waaraan een nieuwe instructie is toegevoegd. Met deze instructie (#invokedynamic) kun je runtime linken, waardoor het type van een doelobject van een methode niet op voorhand bekend hoeft te zijn. Voor de implementaties van talen met dynamische typen, zoals JavaScript, is dit een verbetering ten opzichte van de eerdere JVM’s. JRuby was bijvoorbeeld één van de eerste, die #invokedynamic ten volle benutte. Vanuit dit perspectief is Nashorn als een ‘proof of concept’ te zien dat de JVM nu geschikt is voor dynamische typen.

Er zijn goede redenen om een (script)taal te maken voor de JVM. De JVM is betrouwbaar en uitgebreid getest en wordt veel gebruikt in diverse omgevingen. Dit neemt de ontwikkelaar van een scripttaal veel werk uit handen, omdat je de vertaling naar een specifiek platform en allerlei optimalisaties niet hoeft te ontwikkelen. Bovendien kun je de Java Libraries hergebruiken en zijn deze ‘native’ beschikbaar.

Achtergrond
JavaScript op de JVM heeft een lange voorgeschiedenis. Al in 1997 begon Netscape aan het project Rhino als onderdeel van een groter project om de webbrowser van Netscape geheel in Java te schrijven. Ondanks het feit dat het project niet werd afgerond, was Rhino wel voltooid. Andere bedrijven hebben toen licenties op Rhino genomen, zodat ze het in hun producten konden opnemen. Eén van die bedrijven was Sun Microsystems. Hierdoor kon de ontwikkeling blijven doorgaan. De belangstelling is begrijpelijk, want scripttalen met dynamische typen, zoals JavaScript, hebben hun voordelen. Ze zijn heel geschikt voor prototyping vanwege de hoge productiviteit. Ook de mogelijkheid om runtime code te wijzigen kan een voordeel zijn bij bijvoorbeeld rule-based engines of embedded devices. In de loop van de tijd zijn er dan ook meer dan 240 talen ontwikkeld voor de JVM. 

In 2006 is JSR-292 (”Supporting Dynamically Typed Languages on the JavaTM Platform”) ingediend. Naar aanleiding daarvan is het Da Vinci-project gestart. Dit project onderzoekt welke uitbreidingen er mogelijk zijn voor de JVM, die de sterke punten in de architectuur van de JVM behouden, maar wel meer constructies toestaan die haar geschikter maken voor andere (vooral dynamische) talen. Er zijn deelprojecten gestart voor dynamic invocation, continuation, tail-calls en tail-recursion, interface injection en lightweight method calls. Het werk binnen het Da Vinci-project heeft bijgedragen aan de toevoeging van de instructie #invokedynamic aan Java SE 7. Dit was de eerste keer dat een instructie is toegevoegd aan de JVM-specificatie. Wellicht volgen er nog andere uitbreidingen in de toekomst.

Implementatie dynamische typen
De belangrijkste vernieuwing van Nashorn, ten opzichte van Rhino, is de technische implementatie. Het is interessant om daar wat uitgebreider naar te kijken. De ‘oude’ JVM zonder #invokedynamic heeft een probleem met dynamische typen. De JVM is ontworpen voor één taal, waarbij het type tijdens compile-tijd bekend moet zijn. Bij JavaScript en soortgelijke scripttalen is het type pas runtime bekend. Aangezien Java een generieke taal is, kun je dynamische invocatie wel nabootsen. Het onderstaande voorbeeld van een functie in een hypothetische dynamische taal illustreert het verschil:


function min(x,y) { if y.lessThan(x)
then y else x }

Kenmerkend is dat de ‘receiver’ en de ‘argumenten’ geen typen kennen, waardoor je de code niet direct in bytecode kunt vertalen. Er is immers niet voldaan aan de voorwaarden voor statische methode-aanroepen. Dit kun je op een aantal manieren oplossen. Voor de aanroep van een functie kun je een Java-class aanmaken (DynamicMethod of iets dergelijks) die de functie uitvoert en een resultaat teruggeeft. Je kunt ook de reflectie API (java.lang.reflect.Method) gebruiken en runtime een methode aan een object koppelen. Het probleem is ook op te lossen door een interpreter te schrijven, die bytecode generatie overbodig maakt. Het nadeel van deze oplossingen is dat het ten koste gaat van de performance. Met name een interpreter kan traag zijn. Het probleem is dat de JVM van oorsprong alleen instructies kent om een methode aan te roepen, die uitgaan van statische typering:

  • #invokevirtual: roept een methode aan op een class.
  • #invokeinterface: roept een methode aan op een interface.
  • #invokestatic: roept een static methode aan op een class.
  • #invokespecial: roept een methode aan in speciale situaties, zoals constructors, private methods en super class methods.

De instructie #invokedynamic biedt geheel nieuwe mogelijkheden. Deze instructie lijkt op #invokeinterface, maar in de methodespecificatie hoef je alleen een methodenaam en signatuur toe te voegen en niet een ontvangende class. Dit wordt runtime bepaald door een dynamisch koppelingsmechanisme. De werking hiervan wordt in hoofdlijnen geïllustreerd in figuur 1. Zodra je #invokedynamic voor de eerste keer uitvoert, wordt een bootstrap-methode aangeroepen, die een java.lang.invoke.CallSite teruggeeft. Deze CallSite verwijst naar een MethodHandle, die te vergelijken is met een function pointer, waarmee je het doelobject kunt veranderen. De performancewinst is erin gelegen dat de JVM de aanroepen naar dynamische CallSites kan optimaliseren en minder controles uitvoert.

Het onderstaande voorbeeld verduidelijkt het concept verder. Aan een Callsite kun je de method ‘toUpperCase’  meegeven om daarna het ‘target’ van de CallSite te wisselen.


MutableCallSite callSite = new MutableCallSite(MethodType.methodType(String.class));
MethodHandle methodHandle = callSite.dynamicInvoker();
MethodType methodHanldeMethodType = MethodType.methodType(String.class);
MethodHandle methodHandleToUpper = MethodHandles.lookup().findVirtual(String.class, "toUpperCase", methodHanldeMethodType);

 


MethodHandle worker = MethodHandles.filterReturnValue(methodHandle, methodHandleToUpper);
callSite.setTarget(MethodHandles.constant(String.class, "Rocky"));
//Print ROCKY
System.out.println((String) worker.invokeExact()); callSite.setTarget(MethodHandles.constant(String.class, "Fred"));
//Print FRED
System.out.println((String) worker.invokeExact());

 

MethodHandle heeft ook nog allerlei andere interessante mogelijkheden voor dynamische aanroepen. De API lokt enige creativiteit uit.

Lifecycle Nashorn
Nashorn kent vijf fasen; lexer (deze vertaalt de karakters uit de broncode in een reeks symbolen), parser, codegeneratie, loading en runtime. Er wordt gebruikgemaakt van een ‘recursive descent parser’ die een tussentijdse representatie van de code maakt door de Abstract Syntax Tree/ Intermediate Representation (AST/IR).

De codegenerator maakt in twee stappen van de AST/IR JVM-bytecode. De eerste stap is een transformatie en optimalisatie, die de AST/IR dichter bij de JVM-bytecode brengt. In de tweede stap vertaalt de codegenerator de AST/IR in bytecode via de ASM-library, die er een class van maakt die de script loader kan cachen en gebruiken.

Hierna kun je de class in de JVM laden. Daarvoor wordt de methode defineClass via een secured class loader aangeroepen, die een array van bytes vertaalt in een instantie van een class. Op deze manier kun je in Java efficiënt een gecompileerde class gebruiken.

Ter ondersteuning van het compilatieproces gebruikt Nashorn een aantal libraries. De linker library roept invokedynamic aan met de java.lang.invoke API (JSR 292). De JavaScript Objects Library biedt ondersteuning voor alle JavaScript-objecten zoals Object, Function, Number, String, Date, Array, RegExp, enz.

Script API
Betere performance is natuurlijk heel handig, maar hoe kun je deze benutten? De interactie tussen scripts en Java is gestandaardiseerd in JSR 223, die sinds Java SE 6 is opgenomen. De doelstelling van JSR 223 was oorspronkelijk om server-side webscripts mogelijk te maken. In de specificatie wordt PHP als referentietaal genoemd, maar uiteindelijk is het een meer generieke standaard geworden.

De Scripting API werkt twee kanten op. Het stelt ontwikkelaars in staat om scripts aan te roepen vanuit Java en om Java-methoden aan te roepen vanuit scripts. De Java API specificeert ‘Java Scripting Engines’, waarmee je scripts van verschillende talen kunt aanroepen. De specificatie beschrijft echter niet hoe je de ‘binding’ met Java-objecten moet implementeren en laat dat over aan de specifieke implementatie van een scripttaal. De implementatie van Nashorn is te vinden in de package jdk.nashorn.api.scripting.

Een aantal classes en interfaces uit JSR223 (javax.script), die je kunt gebruiken om scripts vanuit Java aan te roepen zijn:

Een optionele interface waarmee je functies kunt uitvoeren die in scripts zijn gedefinieerd.

ScriptEngineManager ‘Discovery’ en ‘Instantiation’ voor ScriptEngine
ScriptEngine De interface die een implementatie moet ondersteunen.
Bindings String key/value paren om gegevens uit te wisselen tussen Java en scripts.
Compilable Een optionele interface waarmee je een script kunt compileren
Invocable

Het onderstaande voorbeeld geeft aan hoe dit in zijn werk gaat. Een ScriptEngine laadt een script dat aangeroepen kan worden als Invocable.


ScriptEngine se = new ScriptEngineManager().getEngineByName("nashorn");
se.eval("function f(x) { return 2*x} ");
Invocable i = (Invocable)se;
Double result= (Double)i.invokeFunction("f",5);

Een andere mogelijkheid van JSR 223 is om een script aan een Java-interface te koppelen met de methode ‘getInterface’ van ScriptEngine. In het volgende voorbeeld wordt een JavaScript-postcodevalidatie gekoppeld aan een interface in Java

 

Javascript


/**
* Valideer een postcode.
*/
function valideerPostcode(postcode)
if(typeof(postcode)!='string') { return false }
return postcode.match(/^[A-Za-z]{4,4}\s+[0-9]{2,2}$/);
}

Interface


public interface Validatie { 
boolean valideerPostcode(String postcode);
}

 

Java


        ScriptEngineManager manager= new ScriptEngineManager();
ScriptEngine e = manager.getEngineByName("nashorn");
e.eval(new FileReader(Paths.get("javascript/validatie.js").toFile()));
Invocable validatieScript=(Invocable)e;
Validatie validatie=validatieScript.getInterface(Validatie.class);
if(validatie.valideerPostcode("ABCD 12")) { System.out.println("GOED");
}
else
{
System.out.println("FOUT");
}

   

Dit zijn eenvoudige voorbeelden, die een suggestie geven van de mogelijke toepassingen van Script Engines. Deze voorbeelden kunnen echter met een beetje fantasie heel ver gaan.

Conclusie
Nashorn is bovenal een ‘proof of concept’ van een effectief gebruik van #invokedynamic. Dit sluit aan op de ontwikkeling om meer talen te ondersteunen op het Java-platform, zodat je deze flexibeler kan inzetten. De performance is cruciaal voor het succes. Daarover wordt gezegd dat deze performance redelijk is, maar dat er in de nabije toekomst grote stappen gemaakt zullen worden.

Voor een succesvolle acceptatie is tooling ook belangrijk. Voor JavaScript is deze ruimschoots voorhanden, maar voor Nashorn is ze beperkt. Wel heeft NetBeans 8 een JavaScript-debugger. Het blijft dus voorlopig behelpen, maar dat wordt waarschijnlijk snel beter.

De techniek waar Nashorn gebruik van maakt, is het meest interessant. Deze heeft namelijk veel nieuwe mogelijkheden en toepassingen. Het is zeker iets om te blijven volgen. De techniek geeft een verfrissende en innoverende kijk op het gebruik van Java. Mijn advies: probeer het zelf eens uit!

Bibliografie

ECMAScript 5.1 specificatie. (sd).
Opgehaald van ECMA International: http://www.ecma-international.org/ecma-262/5.1/

Java: Dynamic Language Support on the Java Virtual Machine. (sd).
Opgehaald van http://www.oracle.com/technetwork/issue-archive/2010/10-may/o30java-099612.html

JSR 223: Scripting for the JavaTM Platform. (sd).
Opgehaald van Java Community Process: https://jcp.org/en/jsr/detail?id=223

JSR 292: Supporting Dynamically Typed Languages on the JavaTM Platform. (sd).
Opgehaald van Java Community Process: https://jcp.org/en/jsr/detail?id=292

Lindlom, T. Y. (2013).
The Java® Virtual Machine Specification Java SE 7 Edition. Redwood: Oracle America Inc.

Nashorn JDK Enhancement Proposal (JEP 174). (sd).
Opgehaald van http://openjdk.java.net/jeps/174

NasHorn Repository. (sd).
Opgehaald van http://hg.openjdk.java.net/nashorn/jdk8/nashorn

NasHorn User Guide. (sd).
Opgehaald van http://download.java.net/jdk8/docs/technotes/guides/scripting/nashorn/toc.html

OpenJDK Project NasHorn. (sd).
Opgehaald van http://openjdk.java.net/projects/nashorn/

Rose, J. R. (2009).
Bytecodes meet Combinators: invokedynamic on the JVM. VMIL '09 Proceedings of the Third Workshop on Virtual Machines and Intermediate Languages, Artikel No. 2.