Rozdział 24. Wykorzystywanie Javy wraz z innymi językami programowania

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:

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.

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.

Więcej informacji na temat klasy java.lang.ProcessBuilder można znaleźć w jej dokumentacji.

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.

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.

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-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.

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).

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/.

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.

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ń.

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.

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.

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.

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ć”.

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:

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/.