Jak wywołać z Javy konkretną funkcję skryptu JavaScript

Wczoraj opisałem trochę w jaki sposób można wykonać skrypt JS używając silnika Mozilla Rhino. Wczoraj byłem tak podekscytowany tym, że wreszcie mi się udąło, że zapomniałem napisać z czego właściwie korzystałem 😉

Tak czy inaczej samo wykonanie skryptu to jeszcze nie wszystko. Gdybyśmy chcieli mieć w grze wiele różnych obiektów wykonujących kilka zadań w zależności od potrzeb to musielibyśmy dla każdej funkcji pisać osobny plik skryptu. Ani to wygodne, ani eleganckie dlatego też warto było poświęcić wczoraj kilka chwil na zastanowienie się w jaki sposób wywoływać po nazwie funkcje JS z kodu Javy.

Aby to osiągnąć najpierw trzeba załadować skompilowany skrypt tak jak w ostatnim poście.

URL[] urls;
urls = new URL[] { new File('/home/morti/NetBeansProjects/RhinoJS/scripts').toURI().toURL() };
URLClassLoader loader = new URLClassLoader(urls);
Class cl = Class.forName('Base', true, loader);

Script scr = (Script) cl.newInstance();

Następnie należy utworzyć/pobrać kontekst wykonywania oraz przestrzeń:

Context cx = Context.enter();
Scriptable scope = cx.initStandardObjects();

Na stronie o silniku RhinoJS nie rozwodzili się przesadnie nad tym czym jest kontekst ale dość jasno zaznaczyli, że ma on występować jeden na wątek. Na szczęście funkcja enter() załatwia wszystko za nas. Jeżeli w tym wątku nie ma jeszcze kontekstu to zostanie utworzony a jeśli jest to zostanie pobrany i zwiększony jego licznik referencji. Skoro jest licznik referencji to pewnie trzeba też jakoś informować o oddaniu obiektu no i rzeczywiście: mamy metodę Context.exit().

A do czego te przestrzenie? No cóż. Służą one głównie do przechowywania środowiska wykonywania skryptu (np. zdefiniowanych zmiennych oraz ich wartości). Możemy mieć takich przestrzeni ile dusza zapragnie no i dobrze 🙂 Cała sztuczka polega na tym, że dla każdego obiektu w grze będziemy mieli jedną przestrzeń, która będzie przechowywała stan zmiennych oraz treści funkcji. Za chwilę postaram się wytłumaczyć dlaczego tyle tych scopów będziemy potrzebować. Najpierw jednak dam następny fragmencik kodu:

scr.exec(cx, scope);

Być może dla niektórych jest już jasne dlaczego potrzebujemy wiele przestrzeni. Napiszę jednak, aby wszyscy mogli wyciągnąć z tego tekstu wartościowe informacje 🙂 Jak widzimy powyżej należy wykonać załadowany skrypt. Trzeba wykonać ten krok aby zdefiniować w przestrzeni funkcje. Jeśli będziemy mieli wiele obiektów używających skryptów tej samej postaci (z tymi samymi nazwami funkcji) to gdybyśmy zdefinoiwali je wszystkie w jednej przestrzeni to tylko funkcje ostatniego skryptu byłyby wykonywane dla wzsystkich gdyż nadpisałyby one definicje poprzednich skryptów. Dlatego każdy obiekt w grze powinien mieć włąsną przestrzeń wykonywania skryptu.

Jak dotąd w sumie zrobiliśmy to samo co w ostatnim poście z tą różnicą, że dziś na tym nie skończymy 😉 No i oczywiście opisałem nieco znaczenie kilku wymaganych linijek. W całym kodzie trzeba oczywiście jeszcze umieścić odpowiednie bloki try{..} catch(…) ale nie używam tego tutaj ponieważ utrudniłoby to tylko czytanie przykładów.

Ostatnią rzeczą, którą trzeba wykonać jest pobranie obiektu funkcji z przestrzeni oraz wywołanie jej z podaniem parametrów:

Object fObj = scope.get('NazwaFunkcji', scope);
if(fObj instanceof Function) {
	Function f = (Function)fObj;

	result = f.call(cx, scope, scope, new Object[] {'Argument1', new Integer(44), arg3});
	System.out.println('Result: ' + result);
}

Powstaje naturalne pytanie dlaczego podajemy aż 2 razy scope skoro na zdrowy rozum możnaby go nie podawać wcale! Pobraliśmy przecież obiekt z tej przestrzeni więc o co tu chodzi. No cóż. chcąc być do końca szczerym to powiem, że nie zagłębiałem się w te przestrzenie tak mocno aby potrafić udzielić zadowalającej odpowiedzi. Mogę jedynie wkleić opis z dokumentacji:

java.lang.Object call(Context cx,
                      Scriptable scope,
                      Scriptable thisObj,
                      java.lang.Object[] args)

    Call the function. Note that the array of arguments is not
    guaranteed to have length greater than 0.

    Specified by:
        call in interface Callable

    Parameters:
        cx - the current Context for this thread
        scope - the scope to execute the function relative to. 
                This is set to the value returned by getParentScope() 
                except when the function is called from a closure.
        thisObj - the JavaScript this object
        args - the array of arguments 
    Returns:
        the result of the call

Być może komuś pryda się opisany tutaj sposób na wywołanie funkcji ze skompilowanych skryptów JavaScript. Może ktoś wie coś więcej i potrafi dodać coś do tematu? Ja z pewnością skorzystam z umiesczenia tutaj tego przykładu bo nie będę musiał go szukać w kodzie 😉

Reklamy

Java Scripting with JavaScript

Jakiś czas temu pisałem o tym jaki to Jython jest świetny ze względu na to, że potrafi się świetnie integrować z Javą itd. Nadal uważam, że to dobra rzecz, ale po pewnym czasie używania tegoż zaczęła mnie irytować pewna sprawa – czas uruchamiania!

Na początku było tak, że za każdym razem przy uruchamianiu aplikacji kompilowałem skrypty. Myślałem wówczas, że tworzenie interpretera i kombinowanie da się później wyeliminować kompilując skrypty do plików *.class. Tak się niestety nie stało.

Ostatnio usiłując poprawić czas uruchamiania aplikacji skompilowałem najpierw te kilka skryptów testowych do plików binarnych klas Javy i spróbowałem je załadować zupełnie pomijając tworzenie obiektu klasy PythonInterpreter. Życie nie okazało się jednak łaskawe i jak się okazało Jython utworzył mi klasę NazwaKlasy$py.class. Wszelkie próby załadowania tej klasy bez użycia Jythona skończły się niepowodzeniem… Sam Jython oczywiście potrafił bez problemów wczytać ten plik ale zajmowało to tyle samo czasu ile wcześniej (ok. 5 sekund), zatem startowanie aplikacji nie przyspieszyło.

Postanowiłem zatem poszukać pomocy gdzie indziej – zmienić język skryptowy. Po krótkim poszukiwaniu stwiedziłem, że chyba najbardziej trafnym językiem skryptowym dla Javy będzie.. JavaScript 🙂
Szybko zabrałem się za pisanie testowych skryptów i kodu uruchamiającego. Kiedy opanowałem samo wykonywanie, przyszedł czas na kompilację i ładowanie. Po kilkudziesięciu minutach z dokumentacją i przykładowymi kawałkami kodu udało mi się stworzyć program kompilujący plij *.js do pliku *.class. Okazało się to być znacznie prostrze niż przypuszczałem gdyż wystarczył taki fragmencik:

String script = readSource("scripts/base.js");
CompilerEnvirons env = new CompilerEnvirons();
ClassCompiler classCompiler = new ClassCompiler(env);
Object[] classes = classCompiler.compileToClassFiles(script, "scripts/base.js", 1, "Base");

I tyle! Funkcja readScript(..) wczytuje oczywiście z pliku kod skryptu. Wówczas tablica obiektów classes zawiera nazwy klas oraz skompilowane wersje binarne następujące kolejno po sobie. Wystarczy już tylko wpisać dane do odpowiednich plików *.class:

for(int i=0; i < classes.length; i+=2) {
 String name = (String)classes[i];
 byte[] bytes = (byte[])classes[i+1];

 File out = new File("scripts/" + name + ".class");
 FileOutputStream os = new FileOutputStream(out);
 os.write(bytes);
 os.close();
}

Wersja binarna może być już łatwo odczytana przez Javę:

URL[] urls = new URL[] { new File("/path/to/scripts").toURI().toURL() };
URLClassLoader loader = new URLClassLoader(urls);
Class cl = Class.forName("Base", true, loader);

Na koniec aby wykonac skrypt można utworzyć obiekt przy pomocy klasy oraz wywołać funkcję exec():

Script scr = (Script) cl.newInstance();
Context cx = Context.enter();
Scriptable scope = cx.initStandardObjects();
scr.exec(cx, scope);

Na zakończenie powiem tylko, że czasowo kompilacja kilku skryptów nie robi praktycznie żadnej różnicy w działaniu programu. Jest to zatem duży plus dla silnika JavaScriptu od Mozilli 🙂 Warto również dodać, że nie traci się spejcalnie na funkcjonalności ponieważ z poziomu skryptów JS można również korzystać z klas Javy 😉

Później napiszę jeszcze jak ze skryptu wywoływać konkretne funkcje, bo to w sumie nadaje się na osobny wpis 😉