Přejít k navigační liště

Zdroják » JavaScript » Rhino: na rozhraní JavaScriptu a Javy

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.

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.

Komentáře

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Radek

Super, přesně takový přehled o Rhinu jsem potřeboval. Bookmarkuji a děkuji!

AHA

V článku to není zmíněno, tak to raději připomenu: ač jde o projekt pod křídly Mozilly, není pod klasickou trilicencí MPL/LGPL/GPL2, ale jen pod duální MPL 1.1/GPL 2.0. Vypadá to jako pohoda, ale už jsem se setkal s projektem, který byl pro změnu LGPL a Rhino nemohl použít, aniž by musel sám sebe relicencovat.

Ladislav Thon

Pokaždé, když narazím na Rhino, si vzpomenu na Rhino on Rails. Tomu říkám Hack :-)

Jakub D.

Dekuji autorovi za uzasny clanek. Rhino pouzivame v praci a diky tomuto clanku jsem ziskal vydatny uvod do problematiky. Jakmile bude vhodna prilezitost, urcite si s Rhinem pohraji vice.

Jakub D.

V praci vyvijime jednak hlasove aplikace ve VoiceXML, ktere pouziva ECMAScript-327 (odlehcena verze kvuli vykonu) pro semantiku operaci a doplneni pokrocilejsi funkcionality. A pak se zabyvame rozsirovanim platformy IBM VoiceServer o nase vlastni reseni, ktera jsou postavena mj. na Rhinu.

Konkretne mame komponentu, ktera umozni vyvojari VoiceXML aplikace nacist nejakou informaci z Internetu pohodlneji, nez jak se to da udelat normalne. Vyvojar si pritom potrebnou funkcionalitu (napr. zpracovani vysledku) muze upravit predefinovanim danych metod ve zvlastnim .js souboru. Ten je pak interpretovan nasi komponentou, ktera pouziva Rhino, a vysledek se vrati zpusobem kompatibilnim s VoiceXML 2.1 specifikaci (element 'data').

shMoula

Skoncil jsem u "konstrukce break label a continue label – javovská obdoba příkazu goto", i kdyz clanek je to urcite zajimavy. Ale pokud je kod neceho tvoren takovymto zpusobem, tak me prestava zajimat…

oswald

Kdo by si někdo chtěl s propojením Rhyna s Javou pohrát, tomu by se mohl hodit plugin pro jEdit, který umožní scriptovat celý jEdit pomocí JavaScriptu. Také obsahuje JS shell.

Screenshot:
http://www.webkitchen.cz/lab/jEdit/plugins/JavaScriptShell/JavaScriptShell.png

Ukázka kodu:
http://www.webkitchen.cz/lab/jEdit/plugins/JavaScriptShell/startup.js

Download:
http://plugins.jedit.org/plugins/?JavaScriptShell

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.