Rhino: na rozhraní JavaScriptu a Javy

V předchozích dílech našeho seriálu jsme si popisovali implementace JavaScriptu uvnitř webových prohlížečů. Dnes jejich svět opustíme a podíváme se na Rhino, implementaci JavaScriptu v Javě. Představíme si její interpret a kompilátor a především si popíšeme, jak JavaScript umí díky Rhinu spolupracovat s Javou.

Seriál: Do hlubin implementací JavaScriptu (14 dílů)

  1. Do hlubin implementací JavaScriptu: 1. díl – úvod 30.10.2008
  2. Do hlubin implementací JavaScriptu: 2. díl – dynamičnost a výkon 6.11.2008
  3. Do hlubin implementací JavaScriptu: 3. díl – výkonnostně nepříjemné konstrukce 13.11.2008
  4. Do hlubin implementací JavaScriptu: 4. díl – implementace v prohlížečích 20.11.2008
  5. Do hlubin implementací JavaScriptu: 5. díl – implementace mimo prohlížeče 27.11.2008
  6. SquirrelFish: reprezentace hodnot JavaScriptu a virtuální stroj 4.12.2008
  7. SquirrelFish: optimalizace vykonávání instrukcí a nativní kód 11.12.2008
  8. SquirrelFish: regulární výrazy, vlastnosti objektů a budoucnost 18.12.2008
  9. SpiderMonkey: zpracování JavaScriptu ve Firefoxu 8.1.2009
  10. SpiderMonkey: rychlá kompilace JavaScriptu do nativního kódu 15.1.2009
  11. V8: JavaScript uvnitř Google Chrome 22.1.2009
  12. Rhino: na rozhraní JavaScriptu a Javy 29.1.2009
  13. Velký test rychlosti JavaScriptu v prohlížečích 5.2.2009
  14. Javascriptové novinky: souboj o nejrychlejší engine pokračuje 19.3.2009

Historie

Vývoj projektu Rhino započal v roce 1997 ve firmě Netscape. Ta tehdy experimentovala s přepisem svého prohlížeče Netscape Navigator do Javy (projekt Javagator) a k tomu byl potřeba funkční javascriptový engine v tomto jazyce. Vývojáři se tehdy rozhodli do Javy portovat céčkovou implementaci SpiderMonkey, kterou Navigator využíval.

Projekt Javagator vzal časem za své, ale kód javascriptového interpretu měl potenciál dalšího využití. Proto byl v roce 1998 spolu s kódem Netscape Navigatoru zveřejněn jako open source a nyní je spravován pod křídly Mozilly.

Interpret se po celou dobu svého vývoje snažil držet krok se svým bráškou SpiderMonkey, ve kterém v průběhu let přibyla spousta nových funkcí. Docela se mu to dařilo, a tak nyní implementuje JavaScript 1.7 a podporuje různá rozšíření jako je E4X nebo třeba __noSuchMethod__.

Hlavní předností Rhina je jeho dobrá integrace s Javou, což umožňuje především využití knihoven, které jsou v ní napsány. Nejčastěji je dnes Rhino nasazováno na serverech, kde je integrace s existujícím kódem velké plus. Podrobněji jsme si o tom povídali v 5. dílu seriálu.

Jak Rhino funguje?

Rhino má dva režimy – interaktivní a kompilovaný. V interaktivním režimu je skript v JavaScriptu spouštěn jednoduchým interpretem. V kompilovaném režimu je přeložen do bajtkódu Javy, který může být vykonáván jejím virtuálním strojem.

Interpret

Interpret JavaScriptu v Rhinu je klasický zásobníkový virtuální stroj (o typech virtuálních strojů jsme psali ve 3. dílu seriálu). Používá bajtkód s proměnlivou délkou instrukcí – první bajt tvoří opkód (kód instrukce), po něm mohou následovat případné operandy. Dispatching instrukcí je řešen velkým příkazem switch.

Oproti svým kolegům v prohlížečích je interpret Rhina poměrně jednoduchý. Je to dáno především použitým jazykem, který neumožňuje mnohé low-level optimalizace, které jsou v C/C++ snadné.

Kompilátor

Kompilátor Rhina do bajtkódu Javy je oproti interpretu o něco zajímavější. Podporuje několik úrovní optimalizace. V úrovni 0 funguje velmi přímočaře a generuje neoptimalizovaný bajtkód. Ve vyšších úrovních se ale stává chytřejším a snaží se bajtkód zefektivnit. Dokáže například statickou analýzou odhalit a zoptimalizovat proměnné používané pouze jako čísla, eliminovat některé společné podvýrazy, snaží se optimalizovat práci s lokálními proměnnými a parametry funkcí a také urychlit volání metod spekulativním odhadováním jejich cíle. Detaily je možné najít v dokumentaci.

Zdrojový kód

Na zdrojovém kódu Rhina je znát značná snaha o optimalizaci. Byť je psán v Javě, na první pohled vypadá spíš jako kód v céčku. Často jsou v něm používány konstrukce break <label> a continue <label> – javovská obdoba příkazu goto. Musím přiznat, že ve zdrojácích se mi kvůli tomu neorientovalo úplně dobře a troufnu si tvrdit, že interprety v prohlížečích jsou na tom s úrovní kódu o něco lépe.

Integrace s Javou

Jak je již zmíněno výše, Rhino podporuje integraci JavaScriptu s Javou. Ta je možná dvěma směry:

  1. Využití JavaScriptu z Javy. V javovském programu je možné spustit skript v JavaScriptu (načtený například ze souboru) a jeho výsledek dále použít. Spouštěnému skriptu se přitom dají zpřístupnit objekty aplikace, se kterými může pracovat (je to podobné, jako když mají skripty na webových stránkách přístup k objektům DOM prohlížeče). Tento směr integrace se hodí především na přidání podpory skriptování do javovských aplikací.
  2. Využití Javy z JavaScriptu. Skript spouštěný v Rhinu si může importovat libovolné javovské balíčky a používat všechny jejich veřejné třídy. Interpret se přitom snaží zakrýt rozdíly mezi nativními objekty JavaScriptu a objekty Javy. Tento směr integrace se hodí ve chvíli, kdy chcete využít již napsané knihovny v Javě, ale také se takto dají psát třeba javascriptové unit testy k aplikacím.

Pojďme se nyní na oba způsoby integrace trochu podrobněji podívat. Ponecháme přitom stranou povrchní aspekty jako je popis příslušných rozhraní (ty můžete snadno nastudovat z dokumentace), a spíše se zaměříme na to, jak je vše uděláno uvnitř.

Využití JavaScriptu z Javy

Mapování hodnot

Aby mohl program v Javě komunikovat se skriptem v JavaScriptu, je potřeba vyjasnit, jak budou v Javě reprezentovány javascriptové hodnoty. Rhino převádí hodnoty všech javascriptových typů na instance tříd v Javě podle následující tabulky:

Mapování hodnot z JavaScriptu do Javy
Typ v JavaScriptu Reprezentace v Javě
Undefined org.mozilla.ja­vascript.Unde­fined
Null null
Boolean java.lang.Boolean
Number java.lang.Number
String java.lang.String
Object org.mozilla.ja­vascript.Scrip­table

Mapování je poměrně přímočaré, za pozornost stojí pouze typy Undefined a Object.

Typ Undefined má jedinou instanci (undefined), která je v Javě mapována na singleton třídy Undefined – zůstává tak zachována vlastnost jedinečnosti.

Typ Object je reprezentován rozhraním Scriptable, které obsahuje především definice metod pro práci s vlastnostmi objektů. Toto rozhraní implementuje třída ScriptableObject, od které jsou odvozeny třídy implementující jednotlivé vestavěné typy objektů v JavaScriptu (Object, Array, Date atd.). Pokud byste chtěli skriptu zpřístupnit nějaký vlastní objekt, je také potřeba ho obalit do třídy odvozené od ScriptableObject.

Vlastnosti uvnitř objektů jsou uloženy v hash tabulce, přičemž poslední přístup k nim je kešován. Pokud tedy například 2× za sebou napíšete print(obj.x), vlastnost x se bude v tabulce objektu obj vyhledávat jen jednou.

Vnitřní vs. vnější reprezentace

Výše popsané mapování javascriptových hodnot na instance tříd v Javě platí především pro rozhraní s vnějším světem. Uvnitř se Rhino kvůli rychlosti snaží co nejvíce využívat javovské primitivní typy, především boolean a double. Interpret Rhina tak například obsahuje dva paralelní zásobníky, přičemž na jednom jsou ukládány obecné hodnoty (reprezentované referencemi na objekty podle tabulky výše) a na druhém pouze čísla (hodnoty typu Number reprezentované primitivním typem double). První zásobník v případě uložení čísla obsahuje pouze příznak, že uloženou hodnotu je třeba hledat na druhém zásobníku. Důsledkem je, že číselné hodnoty na zásobníku není nutné „zabalovat“ do objektů, což šetří alokace paměti a dereference ukazatelů.

Paralelní zásobníky v Rhinu

Paralelní zásobníky v Rhinu. Hodnota DBL_MRK na obecném zásobníku znamená, že na dané pozici je uloženo číslo, jehož hodnotu lze nalézt na druhém zásobníku.

Chyby skriptů

Kromě hodnot je potřeba nějak reprezentovat chyby, které může vyvolat spuštěný skript. V Javě se pro vyjádření chybových stavů obvykle používají výjimky a nabízí se tedy možnost chyby JavaScriptu namapovat na ně. Přesně to tvůrci Rhina udělali.

Všechny chyby skriptů jsou reprezentovány výjimkou RhinoException, která obsahuje text chyby, pozici ve zdrojovém kódu a zachycený stav zásobníku. Pokud je chyba výsledkem příkazu throw, je vyvolána výjimka JavaScriptExcep­tion, což je podtřída RhinoException a obsahuje navíc hodnotu, která byla předána příkazu throw.

Příklad

Na závěr této části článku si ukážeme malý příklad – program v Javě, který vyhodnocuje javascriptové skripty předané jako argumenty na příkazové řádce. Po vyhodnocení skriptu buď vypíše jeho výslednou hodnotu, nebo oznámí chybu:

import org.mozilla.javascript.*;

public class JavaScriptTest {
  public static void main(String[] args) {
     /* Inicializace. */
     Context cx = ContextFactory.getGlobal().enterContext();
     Scriptable scope = cx.initStandardObjects();

     try {
       /* Projdeme všechny argumenty a každý vyhodnotíme jako javascriptový
          program. */
       for (int i = 0; i < args.length; i++) {
         try {
           /* Vyhodnotíme... */
           Object result = cx.evaluateString(scope, args[i], "<arg>", 1, null);

           /* ...a vypíšeme výsledek. */
           System.out.println(result.toString());
         } catch (RhinoException e) {
           /* V případě chyby vypíšeme její hlášení. */
           System.err.println("Error: " + e.getMessage());
         }
       }
     } finally {
       cx.exit();
     }
  }
} 

Pokud si kód uložíte jako soubor JavaScriptTes­t.java a zkopírujete si k němu soubor js.jar z Rhina, můžete si program zkompilovat následujícím příkazem (tučně vyznačený text ve všech následujících výpisech je zadán uživatelem):

$ javac -cp js.jar JavaScriptTest.java 

Následně si program můžete vyzkoušet:

$ java -cp .:js.jar JavaScriptTest '1 + 1'
2
$ java -cp .:js.jar JavaScriptTest 'ahoj'
Error: ReferenceError: "ahoj" is not defined. (<arg>#1) 

Využití Javy z JavaScriptu

Nyní se podíváme na opačný směr – jak využít kód v Javě ze skriptu v JavaScriptu. Nejdříve je třeba skriptu říct, které balíčky a třídy chceme používat. To jde udělat pomocí top-level objektu Packages, který má vlastnosti představující jednotlivé balíčky Javy nejvyšší úrovně (java, javax, org atd.). Každý balíček je reprezentován jako objekt typu JavaPackage. Jeho vlastnosti odpovídají podbalíčkům a třídám, které obsahuje, přičemž třídy jsou reprezentovány objektem typu JavaClass.

Jak JavaPackage, tak JavaClass jsou vestavěné objekty interpretu a jsou tedy implementovány v Javě. Seznam vlastností objektů typu JavaPackage (tj. seznam obsažených podbalíčků a tříd) je zjišťován dynamicky pomocí mechanismu reflection.

Strukturu objektů si můžeme názorně ukázat pomocí záznamu sezení v JavaScript shellu – interaktivní javascriptové konzoli, která je součástí Rhina:

$ java -cp js.jar org.mozilla.javascript.tools.shell.Main
Rhino 1.7 release 1 2008 03 06
js> Packages
[JavaPackage ]
js> Packages.java
[JavaPackage java]
js> Packages.java.util.ArrayList
[JavaClass java.util.ArrayList] 

Protože balíček java je často používaný, je k dispozici i top-level proměnná java, která je aliasem Packages.java:

js> java
[JavaPackage java] 

Obsah balíčku (obsažené podbalíčky a třídy) je možné snadno zkopírovat jako proměnné do aktuálního scope pomocí funkce importPackage.

js> importPackage(java.util)
js> ArrayList
[JavaClass java.util.ArrayList] 

Výsledek je velmi podobný, jako kdybychom v Javě použili příkaz import java.util.*.

Vytváření instancí

Instance javovských tříd můžeme vytvořit pomocí volání operátoru new, stejně jako u běžných tříd v JavaScriptu. Obdržíme objekt, který zapouzdřuje instanci příslušné třídy v Javě. Na něm můžeme normálně přistupovat k atributům a volat metody – obojí je pomocí reflection delegováno na zapouzdřený javovský objekt. Parametry a návratové hodnoty metod jsou přitom převáděny mezi javovskými a javascriptový­mi typy.

js> list.add("a")
true
js> list.add("b")
true
js> list.add("c")
true
js> list.size()
3 

Rozhraní

Jedním z velkých mínusů Javy jakožto jazyka je absence referencí na metody (známých třeba jako delegáti v C#). Pokud chceme někam předat kus kódu, je potřeba vytvořit rozhraní o jedné metodě a (typicky anonymní) třídu, která toto rozhraní implementuje, což je značně nepohodlné. Protože předávání takových rozhraní je v Javě poměrně časté a pro programátory v JavaScriptu by to bylo nepřirozené, umožňuje Rhino všude tam, kde je očekáváno rozhraní s jednou metodou, předat funkci. Ta se pak zavolá při zavolání oné jediné metody rozhraní.

Jako příklad si můžeme ukázat filtrování seznamu souborů v aktuálním adresáři. Nejdříve si naimportujeme potřebný balíček a připravíme si objekt pro práci s aktuálním adresářem:

js> importPackage(java.io)
js> var dir = new File(".") 

Nyní zavoláme metodu File.list, která vytvoří filtrovaný výpis souborů v daném adresáři. Tato metoda si jako parametr bere rozhraní FilenameFilter, které implementujeme pomocí funkce. Ta bude vracením true nebo false rozhodovat o tom, které soubory se nakonec ve výpisu objeví a které nikoliv:

js> var files = dir.list(function(dir, name) {
  >   return name.endsWith(".jar");
  > })
js> files.length
2
js> files[0]
js.jar
js> files[1]
js-14.jar 

Další informace

Jak je vidět, využití Javy z javascriptových skriptů je poměrně snadné a pro programátora příjemné. Je to dáno především relativní kompatibilitou obou jazyků (která umožňuje snadno mapovat odpovídající datové typy) a flexibilitou JavaScriptu a jeho implementace v Rhinu (která umožňuje hezky zabalit objekty Javy do javascriptové­ho hávu).

V článku jsme si ukázali jen základní možnosti využití celého mechanismu, zájemci najdou další informace v dokumentaci.

Co nás čeká příště

Příštím dílem náš seriál o implementacích JavaScriptu uzavřeme. Podíváme se v něm na rychlostní testy SunSpider, Dromaeo a V8 Benchmark Suite, které se nejčastěji používají k měření výkonu interpretů v prohlížečích.

Autor je vývojář se zájmem o programovací jazyky, webové aplikace a problémy programování jako takového. Vystudoval informatiku na MFF UK a během studií zde i trochu učil. Aktuálně pracuje v SUSE.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 8

Přehled komentářů

Radek Díky!
AHA Licence Rhina
Ladislav Thon RE: Rhino: na rozhraní JavaScriptu a Javy
Jakub D. RE: Rhino: na rozhraní JavaScriptu a Javy
David Majda RE: Rhino: na rozhraní JavaScriptu a Javy
Jakub D. RE: Rhino: na rozhraní JavaScriptu a Javy
shMoula "programatori" = prasata?
oswald Jav
Zdroj: https://www.zdrojak.cz/?p=2926