Java udostępnia kilka sposobów wykonywania programów napisanych w innych językach programowania. Można wywołać skompilowany program lub wykonywalny skrypt za pomocą metody Runtime.exec()
, jak to pokazałem w „24.1. Uruchamianie zewnętrznego programu”. Niemniej jednak takie rozwiązanie wprowadza zależność od używanej platformy systemowej, gdyż zewnętrze aplikacje mogą być wykonywane wyłącznie w tym systemie operacyjnym, w którym zostały skompilowane. Alternatywnym rozwiązaniem może być wykorzystanie jednego z wielu istniejących języków skryptowych (nazywanych także „językami dynamicznymi”), takich jak: awk, bsh, Clojure, Ruby, Perl, Python, Scala; napisany w nich kod można wykonywać, korzystając z pakietu javax.script
, a przykład takiego rozwiązania przedstawiłem w „24.3. Wywoływanie kodu napisanego w innych językach przy użyciu javax.script”. Można też zejść do poziomu języka C, korzystając z mechanizmów obsługi „kodu rodzimego”, jakimi dysponuje Java, i wywoływać skompilowane funkcje napisane w C lub C++ (patrz „24.6. Dołączanie kodu rodzimego”). Z tego poziomu można wywoływać funkcje pisane niemal we wszystkich dostępnych językach programowania. Nawet nie będę wspominać, że za pośrednictwem gniazd (patrz Rozdział 16.) można wymieniać informacje z programami napisanymi prawie we wszystkich istniejących językach, podobnie jak za pomocą usług HTTP (patrz Rozdział 16.). Posługując się technologią RMI, można się komunikować z programami klienckimi napisanymi w Javie, a za pośrednictwem technologii CORBA — z programami klienckimi napisanymi w wielu innych językach.
Dostępnych jest wiele różnych języków, z których można korzystać w wirtualnej maszynie Javy, takich jak:
BeanShell — współpracujący z Javą język skryptowy ogólnego przeznaczenia.
Groovy — bazujący na Javie język skryptowy, w którym jako w pierwszym języku należącym do ekosystemu Javy wprowadzono domknięcia (ang. closure). Dostępna jest także platforma do szybkiego tworzenia aplikacji internetowych w tym języku, o nazwie Grails, jak również program narzędziowy do budowania oprogramowania — Gradle (patrz „1.8. Automatyzacja zależności, kompilacji, testowania i wdrażania przy użyciu programu Gradle”).
Jython — implementacja języka Python napisana w całości w Javie.
JRuby — implementacja języka Ruby napisana w całości w Javie.
Scala — przeznaczony do wykorzystania wraz z Javą język, który łączy w sobie wszystkie najlepsze cechy języków funkcyjnych i obiektowych.
Clojure — w przeważającej mierze funkcyjny dialekt Lisp-1, przeznaczony do współpracy z Javą.
Renjin (wymawiane jako: „er endżin”) — stosunkowo kompletna kopia pakietu statystycznego R, wyposażona w możliwość skalowania i działania w chmurze, udostępniana jako oprogramowanie o otwartym kodzie źródłowym.
Wszystkie te języki są związane z JVM, a niektóre z nich pozwalają na bezpośrednie wywoływanie skryptów z kodu napisanego w Javie lub na odwrót, i to bez korzystania z pakietu javax.script
. Ich pełną listę można znaleźć w Wikipedii.
Należy się posłużyć jedną z metod exec()
klasy java.lang.Runtime
. Można także utworzyć obiekt ProcessBuilder
i wywołać jego metodę start()
.
Metoda exec()
klasy Runtime
pozwala na uruchomienie programu zewnętrznego. Podawany wiersz wywołania będzie dzielony na łańcuchy znaków przy użyciu obiektu StringTokenizer
(patrz „3.2. Dzielenie łańcuchów na słowa”) i przekazywany do wywołania funkcji systemowej odpowiedzialnej za „wykonanie programu”. Jako przykład takiego rozwiązania przedstawiłem poniżej prosty program używający metody exec()
w celu uruchomienia okienkowego edytora kwrite
[88]. W systemach MS Windows można spróbować uruchomić programy notepad
lub wordpad
, podając pełną ścieżkę dostępu do nich, na przykład c:/windows/notepad.exe (ścieżkę można także zapisać za pomocą znaków odwrotnego ukośnika, jednak w takim przypadku trzeba pamiętać, by używać dwóch ukośników, a nie jednego, gdyż w łańcuchach znaków Javy odwrotny ukośnik jest traktowany jako znak specjalny).
/otherlang/ExecDemoSimple.java
public class ExecDemoSimple {
public static void main(String av[]) throws Exception {
// Uruchamiamy program "notepad" lub inny podobny edytor.
Process p = Runtime.getRuntime().exec("kwrite");
p.waitFor();
}
}
Po skompilowaniu programu i uruchomieniu go zostanie wyświetlone okno z odpowiednim edytorem:
$ javac -d . ExecDemoSimple $ java otherlang.ExecDemoSimple # wykonanie powoduje wyświetlenie okna Kwrite $
Ta wersja metody exec()
zakłada, że ścieżka dostępu do programu nie zawiera znaków odstępu, gdyż ich wystąpienie powoduje nieprawidłowe działanie klasy StringTokenizer
. Aby uniknąć tego potencjalnego problemu, należy użyć przeciążonej wersji metody exec()
, której argumentem jest tablica łańcuchów znaków. Program przedstawiony na Przykład 24-1 umożliwia uruchomienie przeglądarki Netscape Navigator w systemach Windows oraz Unix, zakładając, że została ona zainstalowana w domyślnym katalogu. Jako argument wywołania program przekazuje nazwę pliku pomocy, stwarzając w ten sposób prymitywny mechanizm systemu pomocy przedstawiony na Rysunek 24-1.
Przykład 24-1. /otherlang/ExecDemoNS.java
public class ExecDemoNS extends JFrame { private static final String NETSCAPE = "netscape"; /** Nazwa pliku pomocy. */ protected final static String HELPFILE = "./help/index.html"; /** Stos obiektów procesów; każdy element tego stosu śledzi jeden * wykonywany proces zewnętrzny. */ Stack<Process> pStack = new Stack<>(); /** main - inicjalizacja i uruchomienie */ public static void main(String av[]) throws Exception { String program = av.length == 0 ? NETSCAPE : av[0]; new ExecDemoNS(program).setVisible(true); } /** Ścieżka do pliku wykonywalnego, który chcemy uruchomić */ protected static String program; /** Konstruktor - konfigurujemy wszystko... */ public ExecDemoNS(String prog) { super("ExecDemo: " + prog); String osname = System.getProperty("os.name"); if (osname == null) throw new IllegalArgumentException("no os.name"); if (prog.equals(NETSCAPE)) program = // Na razie tylko Windows lub Unix, // użytkownicy Maców - przepraszam (osname.toLowerCase().indexOf("windows")!=-1) ? "c:/program files/netscape/communicator/program/netscape.exe" : "/usr/local/netscape/netscape"; else program = prog; Container cp = getContentPane(); cp.setLayout(new FlowLayout()); JButton b; cp.add(b=new JButton("Uruchom")); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { runProg(); } }); cp.add(b=new JButton("Czekaj")); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { doWait(); } }); cp.add(b=new JButton("Zakończ")); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { System.exit(0); } }); pack(); } /** Uruchamiamy "system pomocy" we własnym wątku. */ public void runProg() { new Thread() { public void run() { try { // Pobieramy URL do pliku pomocy URL helpURL = this.getClass().getClassLoader(). getResource(HELPFILE); // Uruchamiamy przeglądarkę Netscape Navigator. pStack.push(Runtime.getRuntime().exec(program + " " + helpURL)); Debug.println("trace", "Metoda main po uruchomieniu przeglądarki " + pStack.size()); } catch (Exception ex) { JOptionPane.showMessageDialog(ExecDemoNS.this, "Błąd" + ex, "Błąd", JOptionPane.ERROR_MESSAGE); } } }.start(); } public void doWait() { if (pStack.size() == 0) return; Debug.println("trace", "Czekamy na proces " + pStack.size()); try { pStack.peek().waitFor(); // Czekamy na zakończenie procesu. // (W przypadku pewnych starych programów dla systemu Windows // rozwiązanie to może działać nieprawidłowo). Debug.println("trace", "Proces " + pStack.size() + " został zakończony"); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Błąd" + ex, "Błąd", JOptionPane.ERROR_MESSAGE); } pStack.pop(); } }
W większości bardziej złożonych zastosowań wywołania Runtime.exec()
zastępuje nowsza klasa — ProcessBuilder
. Klasa ProcessBuilderDemo
przedstawiona na Przykład 24-2 korzysta z kolekcji typów ogólnych, by zapewnić możliwość modyfikacji lub zastąpienia środowiska.
Przykład 24-2. /otherlang/ProcessBuilderDemo.java
List<String> command = new ArrayList<>(); ➊ command.add("notepad"); command.add("foo.txt"); ProcessBuilder builder = new ProcessBuilder(command); ➋ builder.environment().put("PATH", "/windows;/windows/system32;/winnt"); ➌ final Process godot = builder.directory( new File(System.getProperty("user.home"))). ➍ start(); System.err.println("Czekając na Godota"); ➎ godot.waitFor(); ➏
➊ Przygotowujemy listę argumentów wiersza poleceń: nazwę programu oraz pliku.
➋ Używamy tej listy do rozpoczęcia konfigurowania obiektu ProcessBuilder
.
➌ Konfigurujemy środowisko obiektu ProcessBuilder
, dodając do niego listę najczęściej używanych katalogów systemu Windows.
➍ Ustawiamy katalog początkowy na katalog główny bieżącego użytkownika i uruchamiamy proces!
➎ Zawsze chciałem wyświetlić ten tekst.
➏ Czekamy na zakończenie naszej niewielkiej sztuki.
Więcej informacji na temat klasy java.lang.ProcessBuilder
można znaleźć w jej dokumentacji.
Należy się posłużyć metodą getInputStream()
obiektu Process
, a następnie odczytać i skopiować w dowolnie wybrane miejsce (na przykład do strumienia System.out
) zawartość uzyskanego strumienia.
Według początkowych założeń standardowy strumień wyjściowy oraz standardowy strumień błędów miały być podłączone do „terminala” — rozwiązanie to pochodzi jeszcze z dawnych czasów, kiedy to praktycznie wszyscy użytkownicy komputerów korzystali z wiersza poleceń. Jednak obecnie te standardowe strumienie programu nie pojawiają się automatycznie w każdym miejscu. Można się spierać, że powinien być dostępny automatyczny sposób zapewniający taką możliwość. Jak na razie jednak trzeba dodać do programu kilka wierszy kodu, aby przechwycić i wyświetlić wyniki generowane przez uruchomiony zewnętrzny program.
/otherlang/ExecDemoLs.java
public class ExecDemoLs { /** Program, który należy uruchomić. */ public static final String PROGRAM = "ls"; // "dir" w przypadku Windows /** True, aby zakończyć pętlę. */ static volatile boolean done = false; public static void main(String argv[]) throws IOException { final Process p; // Obiekt process reprezentuje jeden rodzimy // proces. BufferedReader is; // Obiekt, w którym będą zapisywane wyniki // wykonywanego procesu. String line; p = Runtime.getRuntime().exec(PROGRAM); Debug.println("exec", "W metodzie main po wywołaniu exec."); // Opcjonalne: uruchamiamy wątek oczekujący na zakończenie // procesu. Nie będziemy czekać w metodzie main() - tutaj // ustawiamy jedynie flagę "done" i używamy jej do kontroli // działania głównej pętli odczytującej, umieszczonej poniżej. Thread waiter = new Thread() { public void run() { try { p.waitFor(); } catch (InterruptedException ex) { // OK, po prostu kończymy. return; } System.out.println("Program został zakończony!"); done = true; } }; waiter.start(); // Metoda getInputStream zwraca strumień wejściowy (InputStream) // skojarzony ze standardowym wyjściem uruchomionego programu // zewnętrznego (i na odwrót). Użyjemy go do utworzenia obiektu // BufferedReader, dzięki czemu będziemy mogli odczytywać wiersze // tekstu przy użyciu metody readLine(). is = new BufferedReader(new InputStreamReader(p.getInputStream())); while (!done && ((line = is.readLine()) != null)) System.out.println(line); Debug.println("exec", "W metodzie main po zakończeniu odczytu."); return; } }
Powyższe rozwiązanie jest tak często stosowane, że zaimplementowałem je w postaci klasy o nazwie ExecAndPrint
i dołączyłem do mojego pakietu narzędziowego com.darwinsys.lang
. Klasa ExecAndPrint
posiada kilka przeciążonych wersji metody run()
(wszelkie szczegółowe informacje na jej temat można znaleźć w dokumentacji), z których wszystkie wymagają podania co najmniej wykonywanego polecenia, a opcjonalnie także nazwy pliku, w jakim mają być zapisane wyniki. Kod niektórych z tych metod przedstawiłem na Przykład 24-3.
Przykład 24-3. /com/darwinsys/lang/ExecAndPrint.java (fragment kodu)
/** Do wykonania każdej z tych metod potrzebny jest obiekt Runtime. */ protected final static Runtime r = Runtime.getRuntime(); /** Metoda wykonuje polecenie podane w formie łańcucha znaków (String) * i wyświetla jego wyniki w System.out (standardowym * strumieniu wyjściowym). */ public static int run(String cmd) throws IOException { return run(cmd, new OutputStreamWriter(System.out)); } /** Metoda wykonuje polecenie podane w formie łańcucha znaków (String) * i wyświetla jego wyniki w strumieniu "out". */ public static int run(String cmd, Writer out) throws IOException { Process p = r.exec(cmd); FileIO.copyFile(new InputStreamReader(p.getInputStream()), out, true); try { p.waitFor(); // Czekamy na zakończenie procesu. } catch (InterruptedException e) { return -1; } return p.exitValue(); }
W ramach prostego przykładu bezpośredniego wykorzystania metody exec()
oraz klasy ExecAndPrint
przedstawię program, który tworzy trzy pliki tymczasowe, wyświetla zawartość katalogu, a następnie usuwa pliki. Wykonanie programu ExecDemoFiles
powoduje wyświetlenie trzech plików, które wcześniej utworzył:
-rw------- 1 ian wheel 0 Jan 29 14:29 file1 -rw------- 1 ian wheel 0 Jan 29 14:29 file2 -rw------- 1 ian wheel 0 Jan 29 14:29 file3
Kod źródłowy programu zamieściłem na Przykład 24-4.
Przykład 24-4. /otherlang/ExecDemoFiles.java
// Pobieramy i zapamiętujemy obiekt Runtime. Runtime rt = Runtime.getRuntime(); // Tworzymy trzy pliki tymczasowe. rt.exec("mktemp file1"); rt.exec("mktemp file2"); rt.exec("mktemp file3"); // Wykonujemy polecenie "ls" (wydruk zawartości katalogu), // a generowane przez niego wyniki zapisujemy w pliku. String[] args = { "ls", "-l", "file1", "file2", "file3" }; ExecAndPrint.run(args); rt.exec("rm file1 file2 file3");
Proces utworzony i uruchomiony przez program napisany w Javie wcale nie musi być automatycznie kończony w przypadku zakończenia lub nagłego przerwania programu, który go utworzył. Proste programy tekstowe będą kończone, lecz aplikacje „okienkowe”, takie jak programy kwrite
, Netscape Navigator, a nawet programy pisane w Javie i wykorzystujące komponent JFrame
— nie będą. Na przykład przedstawiony we wcześniejszej części rozdziału program ExecDemoNS
uruchamia Netscape Navigatora; po kliknięciu przycisku Zakończ program jest kończony, lecz przeglądarka wciąż działa. A co zrobić, jeśli chcemy uzyskać pewność, że program został zakończony? Obiekt Process
posiada metodę waitFor()
, która pozwala na wstrzymanie realizacji programu aż do momentu zakończenia działania danego procesu zewnętrznego, oraz metodę exitValue()
zwracającą „kod wynikowy” zakończonego procesu. Jeśli natomiast zajdzie potrzeba przerwania realizacji procesu zewnętrznego, to można to zrobić przy użyciu metody destroy()
obiektu Process
. Metoda ta nie wymaga podawania żadnych argumentów ani nie zwraca żadnych wyników. Na Przykład 24-5 przedstawiłem program ExecDemoWait
. Program ten wykonuje dowolny program zewnętrzny, którego nazwa (wraz z argumentami) została podana w wierszu wywołania, przechwytuje standardowy strumień wyjściowy tego programu i czeka na jego zakończenie.
Przykład 24-5. /otherlang/ExecDemoWait.java
// Obiekt Runtime udostępnia metody pozwalające na wymianę // informacji z systemem operacyjnym. Runtime r = Runtime.getRuntime(); Process p; // Obiekt Process "śledzi" jeden zewnętrzny proces. BufferedReader is; // Czytelnik przechwytujący wyniki działania procesu. String line; // argv[0] zawiera nazwę programu, jaki należy wykonać; pozostałe // elementy argv zawierają argumenty, jakie należy przekazać // w wywołaniu procesu zewnętrznego. To wystarczy do wywołania // metody exec wymagającej podania tablicy łańcuchów znaków. p = r.exec(argv); System.out.println("Metoda main po wywołaniu programu zewnętrznego"); // Metoda getInputStream zwraca InputStream (strumień wejściowy) // skojarzony ze standardowym strumieniem wyjściowym procesu zewnętrznego. // Można go wykorzystać do stworzenia obiektu BufferedReader i odczytania // wierszy wyników generowanych przez program zewnętrzny przy użyciu // metody readLine(). is = new BufferedReader(new InputStreamReader(p.getInputStream())); while ((line = is.readLine()) != null) System.out.println(line); System.out.println("Metoda main po odczytaniu wyników"); System.out.flush(); try { p.waitFor(); // Czekamy na zakończenie procesu. } catch (InterruptedException e) { System.err.println(e); // "Niemożliwe". return; } System.err.println("Proces zakończony, kod wynikowy: " + p.exitValue());
Zazwyczaj w przypadku uruchamiania jednego programu napisanego w Javie przez inny program napisany w tym języku metoda exec()
nie będzie wykorzystywana. W takiej sytuacji program zostanie zapewne uruchomiony jako niezależny wątek działający w ramach tego samego procesu, gdyż rozwiązanie to jest zwykle znacznie szybsze (interpreter Javy jest już uruchomiony, a zatem po co mamy czekać na uruchomienie jego kolejnej kopii). Więcej informacji na temat wątków i ich wykorzystania zamieściłem w Rozdział 22.
W przypadku tworzenia poważnych aplikacji o krytycznym znaczeniu należy zwrócić uwagę na umieszczone w dokumentacji Javy ostrzeżenie dotyczące klasy Process
, związane z niebezpieczeństwem utraty danych przesyłanych do lub z programu w wyniku niedostatecznie efektywnego buforowania operacji tego typu przez system operacyjny.
Z poziomu programu napisanego w Javie i wykonywanego przez środowisko wykonawcze Javy chcemy wywołać skrypt napisany w jakimś innym języku, dysponując przy tym możliwością bezpośredniej wymiany danych pomiędzy oboma programami.
Jeśli skrypt został napisany w jednym z ponad dwudziestu obsługiwanych języków, to można go wykonać, korzystając z pakietu javax.script
. Do grupy języków obsługiwanych przez ten pakiet należą między innymi: awk, Perl, Python, Ruby, BeanShell, PNuts, Ksh/Bash, R („Renjin”) oraz kilka implementacji języka JavaScript.
Przykład 24-6 przedstawia bardzo prosty przykład, w którym znamy nazwę mechanizmu skryptowego, jakiego chcemy użyć. R (http://www.r-project.org/) jest doskonale znanym językiem skryptowym wykorzystywanym do obliczeń statystycznych, stanowiącym kopię języka „S” opracowanego przez firmę Bell Laboratories. Renjin (http://www.renjin.org/) jest implementacją języka R napisaną w całości w Javie.
Przykład 24-6. /otherlang/RenjinScripting.java
public static void main(String[] args) throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("Renjin"); engine.put("a", 42); Object ret = engine.eval("b <- 2; a*b"); System.out.println(ret); }
Ponieważ język R, podobnie jak wiele innych interpreterów, traktuje wszystkie liczby jako zmiennoprzecinkowe, wynikiem wyświetlonym przez powyższy program będzie 84.0
.
Istnieje także możliwość określenia zainstalowanych mechanizmów skryptowych i wyboru jednego z nich. Program ScriptEnginesDemo
przedstawiony na Przykład 24-7 wyświetla wszystkie dostępne mechanizmy skryptowe i wykonuje prosty skrypt w domyślnym mechanizmie, którym jest ECMAScript (znany także jako język JavaScript).
Przykład 24-7. /otherlang/ScriptEnginesDemo.java
public class ScriptEnginesDemo { public static void main(String[] args) throws ScriptException { ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); // Wyświetlamy listę dostępnych języków. scriptEngineManager.getEngineFactories().forEach(factory -> System.out.println(factory.getLanguageName())); // Wykonujemy skrypt, używając języka JavaScript String lang = "ECMAScript"; ScriptEngine engine = scriptEngineManager.getEngineByName(lang); if (engine == null) { System.err.println("Nie można znaleźć mechanizmu skryptowego."); return; } engine.eval("print(\"Witamy w języku " + lang + "\");"); } }
Aby zobaczyć pełną listę obsługiwanych języków i pobrać „mechanizmy skryptowe” — interfejsy łączące programy pisane w Javie i w innych językach, należy zajrzeć na stronę http://docs.oracle.com/javase/7/docs/technotes/guides/scripting/ lub https://java.net/projects/scripting.
Aktualnie witryna nie działa jak należy — aby zobaczyć listę mechanizmów skryptowych dostępnych w witrynie java.net, należy pobrać plik na komputerze i ręcznie go otworzyć — serwer WWW przekazuje go, używając niewłaściwego typu MIME.
Listę wszystkich mechanizmów dostępnych w ramach projektu obsługi języków skryptowych można także znaleźć w repozytorium kodów źródłowych na stronie https://java.net/projects/scripting/sources/svn/show/trunk/engines. Kilkanaście innych mechanizmów jest rozwijanych niezależnie od tego projektu; jednym z nich jest mechanizm języka Perl 5, dostępny na serwerach firmy Google: https://code.google.com/p/javaperlscripting/.
Pakiet javax.script
jest w porządku, jednak nie ma jeszcze mechanizmu obsługującego nasz ulubiony język skryptowy.
Można przygotować własny mechanizm. W tym celu należy zaimplementować interfejsy ScriptEngine
oraz ScriptEngineFactory
i uzupełnić je jednym trywialnym plikiem konfiguracyjnym.
Wykorzystanie jednego z istniejących języków skryptowych może się wydawać banalne. I faktycznie może takie być, choć wcale nie musi. Zanim jednak spróbujemy sami zaimplementować obsługę takiego języka, warto sprawdzić, czy nie jest on już dostępny na stronie https://java.net/projects/scripting.
Interfejs ScriptEngine
jest stosunkowo prosty. Definiuje on kilkanaście metod, przy czym sześć z nich to przeciążone wersje popularnej metody eval
. Dostępna jest także abstrakcyjna klasa AbstractScriptEngine
obsługująca wszystkie te przeciążone metody i realizująca kilka dodatkowych operacji, dzięki czemu pozostaje nam zaimplementowanie jedynie czterech metod abstrakcyjnych. Wszystkie metody, które należy zaimplementować, tworząc klasę potomną klasy AbstractScriptEngine
, zostały przedstawione w poniższym przykładzie:
/otherlang/calcscriptengine/CalcScriptEngine.java
public class CalcScriptEngine extends AbstractScriptEngine { private ScriptEngineFactory factory; CalcScriptEngine(ScriptEngineFactory factory) { super(); this.factory = factory; } @Override public Object eval(String script, ScriptContext context) throws ScriptException { System.out.println( "CalcScriptEngine.eval(): Uruchamiam skrypt: " + script); Stack<Integer> stack = new Stack<>(); StringTokenizer st = new StringTokenizer(script); while (st.hasMoreElements()) { String tok = st.nextToken(); if (tok.equals("+")) { return stack.pop() + stack.pop(); } if (tok.equals("-")) { final Integer tos = stack.pop(); return stack.pop() - tos; } if (tok.equals("*")) { return stack.pop() * stack.pop(); } if (tok.equals("/")) { final Integer tos = stack.pop(); return stack.pop() / tos; } // else... sprawdzać inne operatory. // Jeśli to nie operator, to mamy do czynienia z nazwą. // Pobieramy wartość i zapisujemy ją na stosie. stack.push((Integer) context.getAttribute(tok)); } return 0; } @Override public Object eval(Reader reader, ScriptContext context) throws ScriptException { System.out.println("CalcScriptEngine.eval()"); // Metoda powinna odczytać zawartość pliku w formie łańcucha // znaków (String), a następnie zwrócić wartość // eval(scriptString, context); throw new IllegalStateException( "Metoda eval(Reader) jeszcze nie istnieje."); } @Override public Bindings createBindings() { Bindings ret = new SimpleBindings(); return ret; } @Override public ScriptEngineFactory getFactory() { return factory; } }
Nieco dłuższa jest wymagana implementacja interfejsu ScriptEngineFactory
:
/otherlang/calcscriptengine/CalcScriptEngineFactory.java
public class CalcScriptEngineFactory implements ScriptEngineFactory {
private static final String THY_NAME = "SimpleCalc";
@Override
public String getEngineName() {
return THY_NAME;
}
@Override
public String getEngineVersion() {
return "0.1";
}
@Override
public String getLanguageName() {
return THY_NAME;
}
@Override
public List<String> getExtensions() {
ArrayList<String> ret = new ArrayList<>(1);
ret.add("calc");
return ret;
}
@Override
public List<String> getMimeTypes() {
ArrayList<String> ret = new ArrayList<String>(0);
return ret;
}
@Override
public List<String> getNames() {
ArrayList<String> ret = new ArrayList<String>(1);
ret.add(THY_NAME);
return ret;
}
@Override
public String getLanguageVersion() {
return "0.1";
}
@Override
public Object getParameter(String key) {
switch(key) {
case ScriptEngine.ENGINE:
return getEngineName();
case ScriptEngine.ENGINE_VERSION:
return getEngineVersion();
case ScriptEngine.LANGUAGE:
return getLanguageName();
case ScriptEngine.LANGUAGE_VERSION:
return getLanguageVersion();
default:
throw new IllegalArgumentException("Nieznany parametr " + key);
}
}
@Override
public String getMethodCallSyntax(String obj, String m, String... args) {
// TODO Wygenerowane automatycznie...
return null;
}
@Override
public String getOutputStatement(String toDisplay) {
return toDisplay;
}
@Override
public String getProgram(String... statements) {
return statements.toString();
}
@Override
public ScriptEngine getScriptEngine() {
return new CalcScriptEngine(this);
}
}
Ostatnim wymaganym elementem rozwiązania jest plik konfiguracyjny. Koniecznie należy mu nadać nazwę javax.script.ScriptEngineFactory i umieścić go w katalogu META-INF/services w tym samym miejscu, w którym znajdują się nasze pliki JAR. Plik musi zawierać dokładnie jeden wiersz tekstu, określający pełną nazwę klasy wytwórczej. Oto przykład jego treści:
otherlang.calcscriptengine.CalcScriptEngineFactory
Oczywiście, do większości skryptów trzeba będzie przekazywać parametry czy też „zmienne”. Nasz przykład implementuje najprostszy z możliwych kalkulatorów, który rozpoznaje jedynie cztery podstawowe operatory, działa w odwrotnej notacji polskiej i operuje na liczbach całkowitych. Ale to wystarczy do pokazania, że faktycznie można do niego przekazywać wartości, a skrypt zwraca wyniki operacji. Na przykład jeśli skrypt ma następującą postać:
+i j *+
instrukcja nakazuje pobranie wartości i
oraz j
i pomnożenie ich. Wartości obu zmiennych muszą być przekazane, zanim skrypt zostanie wykonany, gdyż inaczej próba jego uruchomienia zakończy się niepowodzeniem. Za określenie wartości zmiennych odpowiadają poniższe wiersze naszego przykładowego programu:
engine.put("i", 99); engine.put("j", 1)(;
Zaimplementowana wersja metody eval()
umieszcza liczby na stosie; następnie odnajdywany jest operator, obie wartości są pobierane ze stosu i wykonywana jest operacja określona przy użyciu jednego z czterech zaimplementowanych operatorów; wynik jest umieszczany na stosie i zwracany. Oczywiście, taki mechanizm nie przyda się do niczego za wyjątkiem tego prostego przykładu, jednak dobrze pokazuje mechanizm przekazywania i odczytu wartości z mechanizmu skryptowego. No i jeszcze jedno — prawidłową odpowiedzią na pytanie o postać wyników generowanych przez program testujący działanie tego mechanizmu skryptowego (/otherlang/ScriptWithCalcDemo.java) jest:
CalcScriptEngine.eval(): Uruchamiam skrypt: i j + Skrypt zwrócił wynik 100
W ramach poznawania zaawansowanych zagadnień w razie pisania mechanizmu dla języka obsługującego kompilację skryptu na jakiś format, który można zapisać w pliku (tak jak program javac
generuje pliki .class, Python — pliki .pyc itd.), należy przyjrzeć się dokładniej interfejsom Compilable
oraz Invocable
.
Ten program może być wykonany jak jeden z elementów repozytorium javasrc (patrz „1.5. Pobieranie przykładów dołączonych do tej książki i korzystanie z nich”). Typowe rozwiązanie, które jednak nie jest wymagane, polega na umieszczeniu w jednym pliku JAR obu klas oraz pliku konfiguracyjnego, co ułatwia ich rozpowszechnianie i instalację.
Teraz istnieje już możliwość używania w aplikacjach pisanych w Javie dowolnych innych języków. Jednak w pierwszej kolejności trzeba się upewnić, że jest dostępna implementacja wybranego języka — można jej poszukać na stronie podanej na początku tej receptury.
Aby wywoływać kod napisany w Javie z poziomu kodu napisanego w języku Perl, należy użyć modułu Inline::Java
. W odwrotnym przypadku, kiedy trzeba wywołać kod napisany w Perlu w programie napisanym w Javie, należy skorzystać z pakietu javax.script
w sposób przedstawiony w „24.3. Wywoływanie kodu napisanego w innych językach przy użyciu javax.script”.
Perl często jest nazywany „językiem łączącym”, który pozwala połączyć w jedną całość elementy pochodzące z różnych miejsc programistycznego świata. Jednak oprócz tego Perl jest pełnowartościowym językiem programowania, doskonale nadającym się do tworzenia oprogramowania. Ogromna liczba dodatkowych modułów zapewnia możliwość korzystania z gotowych do użycia rozwiązań bardzo wielu problemów, a większość z nich jest dostępna bezpłatnie w witrynie CPAN (ang. Comprehensive Perl Archive Network; http://www.cpan.org/). Co więcej, Perl jako język programowania doskonale nadaje się do błyskawicznego tworzenia prototypów. Z drugiej strony tworzenie graficznych interfejsów użytkownika w Perlu, choć bez wątpienia jest możliwe, to jednak nie jest mocną stroną tego języka. Dlatego też może się zdarzyć, że interfejs graficzny aplikacji będziemy chcieli stworzyć w Javie za pomocą biblioteki Swing, a jednocześnie skorzystać z już gotowej logiki biznesowej napisanej w Perlu.
Na szczęście wśród bogatych zasobów archiwum CPAN znajduje się także moduł Inline::Java
, który sprawia, że integracja programów napisanych w Javie i w Perlu jest szybka i łatwa. W pierwszej kolejności załóżmy, że chcemy wywołać kod napisany w Javie z poziomu skryptu Perl. Na naszą przykładową logikę biznesową wybrałem moduł CPAN mierzący podobieństwo dwóch łańcuchów znaków (tak zwaną odległość edycyjną Levenshteina). Pełny kod źródłowy tego skryptu został przedstawiony na Przykład 24-8. Konieczne jest zastosowanie modułu Inline::Java
przynajmniej w wersji 0.44, gdyż wcześniejsze wersje nie obsługują prawidłowo aplikacji wątkowych, co uniemożliwia użycie biblioteki Swing.
Przedstawiony sposób zastosowania modułu wymaga, by kod napisany w Javie został umieszczony bezpośrednio w kodzie skryptu napisanego w Perlu, i to z wykorzystaniem specjalnych separatorów przedstawionych na Przykład 24-8.
Przykład 24-8. /otherlang/Swinging.pl
#! /usr/bin/perl # Wywołujemy kod Javy ze skryptu napisanego w Perlu i na odwrót. use strict; use warnings; use Text::Levenshtein qw(); # Moduł Perl z archiwum CPAN, mierzący podobieństwo łańcuchów znaków. use Inline 0.44 "JAVA" => "DATA"; # Wskaźnik na kod źródłowy w Javie. use Inline::Java qw(caught); # Funkcja pomocnicza do określania typu wyjątku. my $show = new Showit; # Tworzymy obiekt Javy, używając składni Perla $show->show("Kolejny hacker używający Perla "); # Wywołujemy metodę tego obiektu. eval { # Wywołujemy metodę, która odwoła się do kodu w Perlu; # przechwytujemy wyjątki, jeśli takie się pojawią. print "Sprawdzamy: ", $show->match("Japh", shift || "Java"), " (wyświetlone z poziomu kodu w Perlu)\n"; }; if ($@) { print STDERR "Wyjątek:", caught($@), "\n"; if (caught("java.lang.Exception")) { my $msg = $@->getMessage(); print STDERR "$msg\n"; } else { die $@; } } __END__ __JAVA__ // Tu się zaczyna kod w Javie. import javax.swing.*; import org.perl.inline.java.*; class Showit extends InlineJavaPerlCaller { // Rozszerzenie jest potrzebne wyłącznie w przypadku wywoływania // kodu napisanego w Perlu. /** Prosta klasa Javy, którą będziemy wywoływali z kodu napisanego * w Perlu i która będzie wywoływać kod napisany w Perlu. */ public Showit() throws InlineJavaException { } /** Przykładowa metoda. */ public void show(String str) { System.out.println(str + " w kodzie Javy"); } /** Metoda wywołująca kod napisany w Perlu. */ public int match(String target, String pattern) throws InlineJavaException, InlineJavaPerlException { // Wywołujemy funkcję napisaną w Perlu. String str = (String)CallPerl("Text::Levenshtein", "distance", new Object [] {target, pattern}); // Wyświetlamy wyniki. JOptionPane.showMessageDialog(null, "Odległość edycyjna pomiędzy łańcuchami '" + target + "' oraz '" + pattern + "' wynosi: " + str, "Swingując z Perlem", JOptionPane.INFORMATION_MESSAGE); return Integer.parseInt(str); } }
W prostych przypadkach, takich jak ten przedstawiony powyżej, nie trzeba nawet umieszczać kodu Javy w odrębnym pliku — nic nie stoi na przeszkodzie, by w jednym pliku umieścić kod napisany zarówno w Perlu, jak i w Javie. Co więcej, takiego pliku nie trzeba nawet kompilować; wystarczy go wykonać, używając następującego polecenia:
perl Swinging.pl
(Można w nim także umieścić argument, który zostanie potraktowany jak łańcuch znaków). Po kilku chwilach intensywnego przetwarzania zostanie wyświetlone okienko dialogowe informujące, że odległość pomiędzy łańcuchami "Japh"
oraz "Java"
wynosi 2. Jednocześnie w oknie konsoli pojawi się komunikat: "Kolejny hacker używający Perla w kodzie Javy."
. Kiedy zamkniemy okienko dialogowe, zostanie wyświetlony ostatni komunikat: "Sprawdzamy: 2 (wyświetlone z poziomu kodu w Perlu)"
.
W międzyczasie nasz skrypt napisany w Perlu utworzył instancję klasy Showit
, wywołując w tym celu jej konstruktor. Następnie wywołał metodę show()
utworzonego wcześniej obiektu, która to metoda wyświetliła łańcuch znaków. W dalszej kolejności skrypt wywołał metodę match()
, choć w tym przypadku wykonane czynności były nieco bardziej złożone: kod napisany w Javie odwołał się do metody distance
modułu Text::Levenshtine
, przekazując przy tym do niej dwa argumenty, a konkretnie — dwa łańcuchy znaków. Nasza metoda napisana w Javie odczytała wynik wywołania, wyświetliła go w okienku dialogowym, po czym — jakby tego było mało — zwróciła go do głównego programu napisanego w Perlu, który ją wywołał.
Tak się składa, że blok eval { }
jest stosowanym w języku Perl sposobem przechwytywania wyjątków. W tym przypadku wyjątek jest zgłaszany przez kod napisany w Javie.
Kiedy spróbujemy wywołać ten sam program po raz drugi, przekonamy się, że czas jego uruchamiania jest znacznie krótszy; a to zawsze jest dobrą nowiną. Ale dlaczego tak się dzieje? Otóż podczas pierwszego wywołania moduł Inline::Java
przeanalizował kod źródłowy skryptu, skompilował umieszczony w nim kod napisany w Javie i zapisał go na dysku (zazwyczaj tworzony przy tym plik jest umieszczany w katalogu _Inline). Podczas kolejnych uruchomień moduł jedynie sprawdza, czy kod źródłowy uległ zmianie, a jeśli nie, to korzysta z istniejącego już pliku klasowego zapisanego na dysku. (Oczywiście, jeśli kod źródłowy ulegnie zmianie, to w magiczny sposób zostanie on automatycznie skompilowany). Okazuje się jednak, że za kulisami dzieją się jeszcze dziwniejsze rzeczy. Podczas wykonywania skryptu napisanego w Perlu w sposób całkowicie niewidoczny dla użytkownika tworzony i uruchamiany jest serwer Javy, a kod w Perlu i w Javie komunikują się ze sobą z wykorzystaniem gniazd TCP (patrz Rozdział 16.).
Takie połączenie dwóch języków niezależnych od używanej platformy systemowej pozwala na rozwiązanie wielu problemów związanych z przenośnością oprogramowania. Trzeba pamiętać, by podczas rozpowszechniania aplikacji zawierających taki kod Javy umieszczony wewnątrz kodu Perl nie udostępniać wyłącznie kodu źródłowego, lecz także katalog _Inline. (Zaleca się, by bezpośrednio przed udostępnieniem aplikacji opróżnić ten katalog i ponownie zbudować program, gdyż w przeciwnym razie do dystrybucji mogą trafić stare pliki binarne pozostawione w tym katalogu). Każdy komputer docelowy musi powtórzyć magiczne czynności wykonywane przez moduł Inline::Java
, a to z kolei wymaga dostępności kompilatora Javy. Oprócz tego zawsze będzie niezbędny sam moduł Inline::Java
.
Ponieważ Perl udostępnia analogiczne moduły Inline
dla wielu innych języków programowania (zarówno tych całkiem zwykłych, takich jak C, jak i tych egzotycznych, np. BeFunge), można potraktować go nawet jako łącznik zapewniający możliwość korzystania z innych języków programowania w aplikacjach napisanych w Javie — i to niezależnie od tego, czy Perl będzie używany także w innych celach, czy nie. Jestem pewny, że można ciekawie spędzić bardzo wiele czasu na badaniu zawiłości takich rozwiązań.
Wszelkie informacje na temat modułu Inline::Java
można znaleźć w witrynie CPAN (http://www.cpan.org/), jak również w dokumentacji dostarczanej wraz z samym modułem.
Chcemy wywoływać funkcje pisane w języku C lub C++ z poziomu programów Javy, na przykład w celu uzyskania jak największej efektywności działania bądź dostępu do funkcji sprzętowych lub systemowych.
Język Java pozwala na wczytanie do programu napisanego w tym języku kodu rodzimego, czyli skompilowanego. Niby dlaczego mielibyśmy robić takie rzeczy? Jednym z najlepszych powodów jest konieczność uzyskania dostępu do możliwości funkcjonalnych zależnych od używanego systemu operacyjnego bądź też już istniejącego kodu napisanego w jakimś innym języku. Kolejnym dobrym powodem może być szybkość działania: kod rodzimy najprawdopodobniej będzie wykonywany znacznie szybciej niż pliki klasowe Javy; przynajmniej tak jest obecnie. Podobnie jak wszystko inne w Javie, także i te mechanizmy podlegają systemowi zabezpieczeń języka, na przykład aplety nie mogą korzystać z kodu rodzimego.
Mechanizmy obsługi kodu rodzimego zostały zdefiniowane dla kodu pisanego w językach C oraz C++. W przypadku gdy trzeba wywołać kod napisany w innych językach, konieczne będzie stworzenie w C lub C++ fragmentu kodu, który za pomocą mechanizmów systemu operacyjnego przekaże sterowanie do wybranej funkcji lub aplikacji.
Ze względu na operacje zależne od używanego systemu operacyjnego, takie jak interpretacja plików nagłówkowych czy też przydzielanie rejestrów procesora, może się okazać, że używany kod rodzimy będzie musiał zostać skompilowany przez ten sam kompilator C, który posłużył do skompilowania wirtualnej maszyny Javy. Na przykład w systemie Solaris można wykorzystać kompilator SunPro C albo, być może, gcc
. W 32-bitowych systemach MS Windows należałoby użyć kompilatora Microsoft Visual C++ Version 4.xx lub nowszego (aby kompilator był 32-bitowy). W systemach Linux i Mac OS X będzie można skorzystać z dostarczanego kompilatora bazującego na gcc
. W przypadku innych systemów operacyjnych należy się kierować informacjami zamieszczonymi w dokumentacji dostarczanej przez twórców wirtualnej maszyny Javy.
Należy także pamiętać, że szczegółowe informacje podane w tym rozdziale dotyczą technologii JNI (ang. Java Native Interface) języka Java 1.1, która pod niektórymi względami różni się od wcześniejszej wersji tej technologii stosowanej w języku Java 1.0 oraz od interfejsu rodzimego dostępnego w środowisku Javy stworzonym przez firmę Microsoft.
Pierwszym etapem jest napisanie kodu w Javie, który będzie się odwoływać do kodu rodzimego. W tym celu należy wykorzystać słowo kluczowe native
oznaczające, że dana metoda jest „rodzima”, oraz dodać do programu statyczny blok kodu, który pobierze rodzimą metodę, posługując się metodą System.loadLibrary()
. (Tworzenie modułu umożliwiającego dynamiczne pobieranie kodu rodzimego zostało opisane w kroku 5.). Statyczne bloki kodu są wykonywane w momencie, gdy klasa, która je zawiera, jest wczytywana. A zatem pobierając kod rodzimy w takim statycznym bloku kodu, można mieć pewność, że będzie on dostępny w momencie, gdy zechcemy z niego skorzystać.
Zdefiniowane w obiektach zmienne, które kod rodzimy może modyfikować, muszą być poprzedzone modyfikatorem volatile
. Dobrym przykładem, od którego można rozpocząć poznawanie sposobów wykorzystania kodu rodzimego, jest program przedstawiony na Przykład 24-9.
Przykład 24-9. /jni/HelloJni.java
/** * Prosta klasa demonstrująca wykorzystanie Java Native Interface 1.1 */ public class HelloJni { int myNumber = 42; // Zmienna używana do pokazania sposobu // przekazywania argumentów. // Deklaracja klasy rodzimej. public native void displayHelloJni(); // Metoda main aplikacji; wywołuje metodę display klasy rodzimej. public static void main(String[] args) { System.out.println("Uruchamiamy program HelloJni; args.length="+ args.length+"..."); for (int i=0; i<args.length; i++) System.out.println("args["+i+"]="+args[i]); HelloJni hw = new HelloJni(); hw.displayHelloJni(); // Wywołujemy funkcję rodzimą. System.out.println("Z powrotem w Javie, \"myNumber\" ma teraz wartość " + hw.myNumber); } // Statyczne bloki kodu są wykonywane tylko raz, podczas ładowania // pliku klasowego. static { System.loadLibrary("hello"); } }
Drugi krok jest prosty — wystarczy w standardowy sposób skompilować klasę, posługując się programem javac
; w naszym przypadku polecenie, jakiego należy użyć, będzie miało postać javac HelloJni
. Podczas kompilacji tak prostego programu jak nasz nie pojawią się zapewne żadne błędy, jednak gdyby się pojawiły, to należy je poprawić i powtórnie skompilować program.
Kolejnym etapem jest utworzenie pliku nagłówkowego (z rozszerzeniem .h). Do tego celu wykorzystywany jest program javah
:
javah jni.HelloWorld // To polecenie utworzy plik HelloWorld.h
Wygenerowany plik .h jest swoistym „spoiwem”, nie jest on natomiast przeznaczony ani do czytania, ani tym bardziej do edycji. Niemniej analizując jego zawartość, można się dowiedzieć, że nazwa metody języka C składa się z łańcucha znaków Java
, nazwy pakietu (jeśli taki został użyty podczas tworzenia klasy), nazwy klasy oraz nazwy metody:
JNIEXPORT void JNICALL Java_HelloJni_displayHelloWorld(JNIEnv *env, jobject this);
Teraz należy napisać w języku C funkcję, która będzie wykonywać zamierzone operacje. Sygnatura tej metody musi dokładnie odpowiadać sygnaturze zapisanej w pliku nagłówkowym.
Operacje wykonywane przez tę funkcję mogą być całkowicie dowolne. Warto zwrócić uwagę, że w jej wywołaniu są przekazywane dwa argumenty: środowisko JVM oraz uchwyt do obiektu this
. W Tabela 24-1 przedstawiłem typy języka Java oraz odpowiadające im typy języka C (typy JNI), które można wykorzystywać podczas tworzenia kodu funkcji w języku C.
Tabela 24-1. Typy Javy oraz JNI
Typ Javy | JNI | Typ tablicowy Javy | JNI |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| ||
|
|
|
|
|
| ||
|
| ||
|
| ||
|
|
Przykład 24-10 przedstawia pełną implementację funkcji napisanej w języku C. Dzięki przekazaniu do niej obiektu typu HelloJni
funkcja ta inkrementuje jego całkowite pole o nazwie myNumber
.
Przykład 24-10. /jni/HelloJni.c
#include <jni.h> #include "HelloJni.h" #include <stdio.h> /* * Rodzima implementacja metody displayHelloJni. */ JNIEXPORT void JNICALL Java_HelloJni_displayHelloJni(JNIEnv *env, jobject this) { jfieldID fldid; jint n, nn; (void)printf("Witamy z metody rodzimej\n"); if (this == NULL) { fprintf(stderr, "Wskaźnik strumienia wejściowego = null!\n"); return; } if ((fldid = (*env)->GetFieldID(env, (*env)->GetObjectClass(env, this), "myNumber", "I")) == NULL) { fprintf(stderr, "Nieudane wywołanie GetFieldID"); return; } n = (*env)->GetIntField(env, this, fldid); /* Pobieramy myNumber. */ printf("\"myNumber\" ma wartość %d\n", n); (*env)->SetIntField(env, this, fldid, ++n); /* Inkrementujemy wartość! */ nn = (*env)->GetIntField(env, this, fldid); printf("Wartość pola \"myNumber\" wynosi teraz %d\n", nn); /* upewniamy się */ return; }
Następnym etapem jest skompilowanie kodu napisanego w języku C na obiekt, który będzie można wczytać i wykorzystać w naszym programie Javy. Oczywiście konkretne czynności, jakie należy w tym przypadku wykonać, zależą od używanego systemu operacyjnego, kompilatora oraz innych czynników. Poniżej podałem, jak można skompilować plik HelloWorld.c w systemie Windows:
> set JAVAHOME=C:\java # Należy podać poprawną ścieżkę. > set INCLUDE=%JAVAHOME%\include;%INCLUDE% > set LIB=%JAVAHOME%\lib;%LIB% > cl HelloJni.c -Fehello.dll -MD -LD
oraz w systemie Unix:
$ export JAVAHOME=/local/java # Należy podać poprawną ścieżkę. $ cc -I$JAVAHOME/include -I$JAVAHOME/include/solaris \ -G HelloJni.c -o -libhello.so
Na Przykład 24-11 przedstawiłem specjalny plik ułatwiający kompilację programu (tak zwany plik budowy), przeznaczony do użycia w systemach Unix:
Przykład 24-11. Plik budowy przeznaczony do użycia w systemach Unix (/jni/Makefile)
# Plik budowy dla Java Native Methods 1.1, przykłady przygotowane dla # Learning Tree International Course 471/478. # Plik został przetestowany w systemie Solaris dla kompilatorów "gcc" # oraz SunSoft "cc". # Przetestowano go także w systemie OpenBSD z rodzimą wersją Javy # "devel/jdk/1.2" oraz cc. # W przypadku wykorzystania na innych platformach systemowych bez wątpienia # będzie wymagać pewnych przeróbek; proszę poinformować mnie jak dużych! :-) # Ian Darwin, http://www.darwinsys.com # Sekcja konfiguracyjna. CFLAGS_FOR_SO = -G # Solaris CFLAGS_FOR_SO = -shared CSRCS = HelloJni.c # Wartość JAVA_HOME powinna być określona w środowisku. #INCLUDES = -I$(JAVA_HOME)/include -I$(JAVAHOME)/include/solaris #INCLUDES = -I$(JAVA_HOME)/include -I$(JAVAHOME)/include/openbsd INCLUDES = -I$(JAVA_HOME)/include all: testhello testjavafromc # Ta część dotyczy kodu C wywoływanego z poziomu Javy w HelloJni testhello: hello.all @echo @echo "Testujemy kod w Javie \"HelloJni\" wywołujący kod napisany w C." @echo LD_LIBRARY_PATH=`pwd`:. java HelloJni hello.all: HelloJni.class libhello.so HelloJni.class: HelloJni.java javac HelloJni.java HelloJni.h: HelloJni.class javah -jni HelloJni HelloJni.o:: HelloJni.h libhello.so: $(CSRCS) HelloJni.h $(CC) $(INCLUDES) -G $(CSRCS) -o libhello.so # Ta część jest dla kodu Javy wywoływanego z kodu C w javafromc testjavafromc: javafromc.all hello.all @echo @echo "Teraz testujemy HelloJni, używając javafromc zamiast java" @echo LD_LIBRARY_PATH="$(LIBDIR):." CLASSPATH="$(CLASSPATH)" ./javafromc HelloJni @echo @echo "To było, gdybyś nie zauważył, C->Java->C. Rozwiązanie to" @echo "przez przypadek zastąpiło program \"java\" dostarczany w JDK!" @echo javafromc.all: javafromc javafromc: javafromc.o $(CC) -L$(LIBDIR) javafromc.o -ljava -o $@ javafromc.o: javafromc.c $(CC) -c $(INCLUDES) javafromc.c clean: rm -f core *.class *.o *.so HelloJni.h clobber: clean rm -f javafromc
I to już wszystko! Teraz wystarczy wykonać plik klasowy zawierający program główny przy użyciu interpretera Javy. Zakładając, że wszystkie ustawienia wymagane przez używany system operacyjny zostały podane (prawdopodobnie włącznie ze zmiennymi środowiskowymi CLASSPATH
i LD_LIBRARY_PATH
bądź ich odpowiednikami), to wykonanie programu powinno wygenerować następujące wyniki:
C> java jni.HelloJni Uruchamiamy program HelloWorld; args.length=0... // z Javy Witamy z metody rodzimej // z C Wartość pola "myNumber" wynosi 42 // z C Wartość pola "myNumber" wynosi teraz 43 // z C Z powrotem w Javie, "myNumber" ma teraz wartość 43 // z Javy C>
Gratulacje! Właśnie udało się nam wywołać metodę rodzimą. Niemniej jednak ze względu na wykorzystanie tej metody została utracona przenośność programu, gdyż teraz jego uruchomienie będzie wymagało stworzenia odpowiedniego obiektu ładowalnego tworzonego z myślą o konkretnym systemie operacyjnym i platformie sprzętowej. Pomnóżmy {Windows, Mac OS X, Sun Solaris, HP/UX, Linux, OpenBSD, NetBSD, FreeBSD} razy {Intel, Intel-64, Sparc, AMD64, PowerPC, HP-PA}, a dopiero w tym momencie zdamy sobie sprawę ze znaczenia przenośności.
Należy mieć świadomość, że wszelkie problemy z kodem rodzimym mogą i zapewne doprowadzą do przerwania realizacji procesu z poziomu „poniżej” wirtualnej maszyny Javy. Niestety JVM nie może nic zrobić, aby ochronić się przed źle napisanym kodem w językach C i C++. W tym przypadku programista samodzielnie musi zarządzać pamięcią; nie ma bowiem żadnego mechanizmu automatycznego oczyszczania pamięci przydzielanej podczas działania programu przez narzędzia systemowe. Programista bezpośrednio operuje na systemie operacyjnym, a często także na sprzęcie. Dlatego „należy uważać, bardzo uważać”.
Musimy zastosować rozwiązanie odwrotne — wywołać kod napisany w Javie w programie napisanym w C lub C++.
W wersji 1.1 technologia JNI udostępnia interfejs pozwalający na wywoływanie kodu napisanego w języku Java z programów pisanych w językach C lub C++. Rozwiązanie takie wymaga wykonania następujących czynności:
Stworzenia wirtualnej maszyny Javy.
Załadowania klasy.
Odnalezienia odpowiednich metod danej klasy (na przykład main()
) i wywołania ich.
Rozwiązanie to pozwala wykorzystywać możliwości języka Java w starszych programach. Można je stosować z wielu powodów, lecz sprowadza ono Javę do roli języka pomocniczego, rozszerzającego możliwości aplikacji (na przykład pozwala na zdefiniowanie lub odszukanie interfejsu takiego jak Applet
lub Servlet
i daje użytkownikom aplikacji możliwość zaimplementowania interfejsu lub wyprowadzenia klasy potomnej).
Kod przedstawiony na Przykład 24-12 pobiera nazwę klasy przekazaną w wierszu wywołania, uruchamia JVM i wywołuje metodę main()
podanej klasy.
Przykład 24-12. Wykonywanie programu w Javie z programu w C (/jni/javafromc.c)
/* * To program napisany w języku C, który wywołuje program * napisany w Javie. Może on zostać zastosowany jako model * rozwiązania, w którym używamy Javy jako języka pozwalającego na * rozszerzenie już istniejącego oprogramowania. */ #include <stdio.h> #include <jni.h> int main(int argc, char *argv[]) { int i; JavaVM *jvm; /* Używana wirtualna maszyna Javy. */ JNIEnv *myEnv; /* Wskaźnik do środowiska. */ JDK1_1InitArgs jvmArgs; /* Argumenty inicjalizacyjne JNI. */ jclass myClass, stringClass; /* Wskaźnik do typu klasy. */ jmethodID myMethod; /* Wskaźnik do metody main(). */ jarray args; /* To będzie tablica obiektów String. */ jthrowable tossed; /* Obiekt wyjątku, jeśli zostanie zgłoszony. */ JNI_GetDefaultJavaVMInitArgs(&jvmArgs); /* Przygotowujemy wskaźnik argumentu */ /* Teraz można by zmieniać wartości, na przykład: * jvmArgs.classpath = ...; */ /* Inicjalizujemy JVM! */ if (JNI_CreateJavaVM(&jvm, &myEnv, &jvmArgs) < 0) { fprintf(stderr, "Wywołanie CreateJVM zakończone niepowodzeniem.\n"); exit(1); } /* Odnajdujemy klasę o nazwie podanej w argv[1] */ if ((myClass = (*myEnv)->FindClass(myEnv, argv[1])) == NULL) { fprintf(stderr, "Wywołanie FindClass %s zakończone niepowodzeniem.\n", argv[1]); exit(1); } /* Znajdujemy statyczną metodę void main(String[]) podanej klasy. */ myMethod = (*myEnv)->GetStaticMethodID( myEnv, myClass, "main", "([Ljava/lang/String;)V"); /* myMethod = (*myEnv)->GetMethodID(myEnv, myClass, "test", "(I)I"); */ if (myMethod == NULL) { fprintf(stderr, "Wywołanie GetStaticMethodID zakończone niepowodzeniem.\n"); exit(1); } /* Ponieważ wywołujemy metodę main(), musimy do niej przekazać * argumenty wiersza poleceń w formie tablicy łańcuchów znaków * (obiektów String). */ if ((stringClass = (*myEnv)->FindClass(myEnv, "java/lang/String")) == NULL){ fprintf(stderr, "Nie udało się pobrać klasy String!!\n"); exit(1); } /* Tworzymy tablicę łańcuchów znaków, usuwając jeden łańcuch * reprezentujący nazwę programu oraz drugi reprezentujący nazwę klasy. */ if ((args = (*myEnv)->NewObjectArray(myEnv, argc-2, stringClass, NULL))==NULL) { fprintf(stderr, "Nie udało się utworzyć tablicy!\n"); exit(1); } /* Wypełniamy tablicę. */ for (i=2; i<argc; i++) (*myEnv)->SetObjectArrayElement(myEnv, args, i-2, (*myEnv)->NewStringUTF(myEnv, argv[i])); /* I w końcu wywołujemy metodę. */ (*myEnv)->CallStaticVoidMethodA(myEnv, myClass, myMethod, &args); /* I sprawdzamy wyjątki. */ if ((tossed = (*myEnv)->ExceptionOccurred(myEnv)) != NULL) { fprintf(stderr, "%s: Wykryto wyjątek:\n", argv[0]); (*myEnv)->ExceptionDescribe(myEnv); /* Wyświetlamy w standardowym * strumieniu błędów. */ (*myEnv)->ExceptionClear(myEnv); /* W porządku - gotowe. */ } (*jvm)->DestroyJavaVM(jvm); /* Nie sprawdzamy błędów, bo wszystko * już zostało zrobione. */ return 0; }
[88] Program ten jest dostępny wyłącznie w systemie operacyjnym Unix i stanowi jeden z elementów KDE (ang. K Desktop Environment). Patrz http://www.kde.org/.