Java Native Interface

JNIJava Native Interface je rozhraní umožňující propojit kód běžící na virtuálním stroji Javy s nativními programy a knihovnami napsanými v jiných jazycích – např. C, C++, Assembler, apod., které jsou zkompilované pro určitý hardware, případně operační systém. Jedná se tedy o jakýsi převodní můstek, pomocí kterého se můžeme dostat za hranice virtuálního stroje.

JNI není omezeno pouze na jazyk Java. Jako takové je toto rozhraní součástí virtuálního stroje, lze k němu tedy přistupovat z libovolného jazyka překládaného do javovského bytekódu a spouštěného na platformě JVM. Kromě Javy ho tedy lze využít i v jazycích jako je Groovy, Jython, JRuby, apod. Vše, co bude dále řečeno v souvislosti s JNI, tedy platí i pro tyto jazyky.

Využití JNI

I přesto, že nám platforma virtuálního stroje Javy poskytuje velmi bohaté běhové prostředí, nelze se zcela vyhnout situacím, kdy nám její možnosti nestačí. Toto prostředí s sebou totiž nese i hranice, které v programu není možné překročit. Základní rys virtuálního stroje, který nám zajišťuje nezávislost na platformě nižší úrovně, se tak někdy může stát spíše omezujícím faktorem. Typicky k tomuto dochází, pokud potřebujeme:

  • Používat specifické rysy konkrétního operačního systému.
  • Komunikovat s hardwarovými zařízeními.
  • Využít v naší aplikaci starší, nativní kód nebo naopak v nativním kódu přistupovat k aplikaci běžící uvnitř JVM.

V prvním případě se jedná především o situace, kdy chceme uživateli poskytnout komfort, na který je zvyklý z nativních aplikací, zejména grafické uživatelské rozhraní daného operačního systému.

Bez JNI se také neobejdeme, pokud chceme využívat některá specifická hardwarová zařízení, např. čtečky čárových kódů nebo karet, které nejsou přístupná skrz virtuální stroj a pro které výrobce neposkytuje javovské rozhraní.

Stejně tak je pro nás virtuální stroj nepříjemnou komplikací, pokud potřebujeme komunikovat s již existující nativní aplikací, kterou není možné přepsat do jazyka běžícího nad JVM.

Dalším důvodem pro použití tohoto rozhraní může být také fakt, že nativní kód zkompilovaný pro danou platformu se vykonává rychleji, JNI tedy můžeme využít pro zpracování náročných výpočtů, komplikovaných grafických operací, apod.

Historie JNI

Možnost volání nativního kódu je v Javě už od jejího vzniku. Ve verzi Javy 1.0 bylo implementováno rozhraní NMINative Method Interface, které umožňovalo spuštění kódu napsaného v jiném jazyce. Toto rozhraní využívalo i mnoho vlastních tříd Javy, především z balíčků java.io a java.net. NMI však mělo několik nevhodných vlastností, kvůli kterým nebylo možné nativní knihovny používat na různých virtuálních strojích[1].

Ve verzi Javy 1.1 se proto objevilo nové rozhraní JNI, které tyto nedostatky odstranilo. Na rozdíl od NMI ho podporují všechny virtuální stroje a nativní knihovny je možné používat s různými virtuálními stroji bez rekompilace. Ve verzích Javy 1.2 a 1.4 pak bylo toto rozhraní výrazným způsobem rozšířeno, změny však zachovávají zpětnou kompatibilitu.

Role JNI

Java Native Interface představuje obousměrné spojení mezi nativním kódem a kódem napsaným v Javě. Rozhraní je tedy možné využít dvěma způsoby:

  • Prvním z nich je volání nativního kódu z javovské aplikace. V aplikaci vytvoříme tzv. nativní metodu, kterou pak voláme stejným způsobem, jako jakoukoliv jinou metodu napsanou v Javě. Nicméně ve skutečnosti je prostřednictvím JNI spuštěna metoda, jejíž implementace je napsaná v jiném jazyce a je uložená v nativní knihovně.
  • Druhou možností je pak spuštění virtuálního stroje přímo v nativní aplikaci. V nativním kódu vytvoříme propojení na knihovnu, která implementuje virtuální stroj Javy, a pomocí JNI v něm pak můžeme spouštět části aplikace napsané v Javě. Pokud bychom programovali např. internetový prohlížeč v jazyce C++, pomocí JNI bychom v něm takto mohli spouštět applety.

Nevýhody JNI

Pokud se rozhodneme využívat JNI pro volání nativních funkcí z kódu napsaného v Javě, je třeba mít na paměti, že to s sebou nese jisté důsledky:

  • Především tímto ztrácíme jeden ze základních benefitů jazyka Java, a to podpora nezávislosti na platformě. I když část aplikace napsaná v Javě bude stále přenositelná, nativní knihovny bude nutné znovu přeložit pro každé hostitelské prostředí.
  • Dalším výrazným negativem je také fakt, že Java umí být typově bezpečná, zatímco některé nativní jazyky, jako např. C nebo C++, ne. Z tohoto důvodu je nutné věnovat aplikacím využívajícím JNI zvláštní pozornost. Drobná chyba v implementaci nativní metody může způsobit pád celé aplikace. Navíc tyto chyby bývá obvykle velmi těžké ladit nebo reprodukovat.
  • Kromě toho rozhraní JNI také neposkytuje žádné prostředky pro automatické uvolňování nepotřebných zdrojů z paměti. Pokud nativní kód alokuje nějaké prostředky, nese také zodpovědnost za jejich uvolnění ve chvíli, kdy již nejsou dále potřebné.

Jak JNI funguje

Aby bylo možné z kódu napsaného v Javě zavolat funkci nativní knihovny, je nutné udělat několik kroků[2]. Celý proces je možné shrnout v následujících bodech:

  1. Vytvoření javovské třídy, která deklaruje nativní metodu.
  2. Přeložení této třídy do byte kódu.
  3. Vytvoření hlavičkového souboru se signaturou příslušné metody pro její následnou implementaci.
  4. Implementace metody např. v jazycích C, C++, apod.
  5. Zkompilování této implementace do nativní knihovny.

Dále jsou tyto kroky podrobněji rozebrány.

Vytvoření třídy deklarující nativní metodu

Nativní metoda se v deklaraci třídy označí klíčovým slovem native a její signatura je ukončena středníkem, podobně jako když definujeme metodu v rozhraní. Je nutné pamatovat na to, že před použitím příslušné metody je nutné nejprve nahrát knihovnu, která obsahuje její implementaci. To je možné provést např. voláním metody System.loadLibrary(). Jednoduchá třída s deklarací nativní metody by tedy mohla vypadat např. takto:

public class HelloWorld {
    private native void sayHello();

    public static void main(String[] args) {
        System.loadLibrary("library");
        new HelloWorld().sayHello();
    }
}

Přeložení třídy do byte kódu

Třídu je v tuto chvíli nutné přeložit, abychom mohli pokračovat dále. Většina vývojových prostředí se o toto samozřejmě postará automaticky, případně je to možné udělat z příkazové řádky jednoduchým zavoláním překladače:

javac HelloWorld.java

Výsledkem pak samozřejmě bude soubor HelloWorld.class s příslušným bytekódem.

Vytvoření souboru se signaturou metody

Abychom mohli metodu v nějakém jazyce naimplementovat, potřebujeme znát její přesnou signaturu. Ta samozřejmě odpovídá definici ze zdrojového kódu Javy. Její zápis v jiných jazycích se od ní však poněkud odlišuje. Pokud budeme metodu implementovat v jazycích C nebo C++, můžeme pro získání hlavičkového souboru se správnou podobou signatury využít nástroj javah. Ten však pro tvorbu hlavičkových souborů využívá již soubory s bytekódem, proto bylo nutné třídu v předchozím kroku nejprve přeložit. Metodu je samozřejmě možné naimplementovat i v jiných jazycích, pro ty ale Java SDK neposkytuje žádné podpůrné nástroje. Na příkazové řádce tedy zavoláme:

javah -jni HelloWorld

Po zpracování dostaneme hlavičkový soubor HelloWorld.h, ve kterém bude mimo jiné uvedena i signatura metody v podobě:

JNIEXPORT void JNICALL Java_HelloWorld_sayHello
  (JNIEnv *, jobject);

Jak je z hlavičky patrné, nativní metoda obdrží od rozhraní JNI dva argumenty, přestože původní metoda deklarovaná ve zdrojovém kódu Javy byla bez parametrů. JNIEnv je ukazatel na rozhraní JNI, jobject potom představuje referenci na javovský objekt, který metodu vyvolal. Tyto dva argumenty obsahuje každá nativní metoda volaná prostřednictvím JNI. Pokud by byly v deklaraci metody v Javě požadovány nějaké parametry, byly by v signatuře uvedeny dále, za těmito dvěma.

Implementace nativní metody

Implementaci nativní metody je možné provést v libovolném jazyce. To jediné, co je potřeba dodržet, je volací konvence jazyka C, tedy způsob a pořadí, jakým jsou do zásobníkového rámce uloženy parametry funkce, apod. Pokud jsme schopni toto zajistit, můžeme metodu klidně napsat v Assembleru nebo kterémkoliv jiném jazyce.

Velice jednoduchá implementace nativní metody v jazyce C++ může vypadat např. takto:

#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"

JNIEXPORT void JNICALL
Java_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
    printf("Hello World!\n");
    return;
}

Metoda pouze vypíše na konzoli řetězec Hello World! a skončí. Argumenty metody v tuto chvíli zcela ignorujeme. Na začátku kódu jsou odkazy na 3 hlavičkové soubory. První z nich obsahuje informace nutné k volání nativních metod prostřednictvím JNI. Při implementaci nativních metod musíme tento soubor vždy vkládat. Druhý soubor je zde jen proto, že v kódu využíváme funkci printf(). Třetí soubor byl vygenerován nástrojem javah a mimo jiné obsahuje signaturu dané funkce.

Překlad nativní knihovny

Pokud máme napsaný zdrojový kód, je nutné ho zkompilovat do nativní knihovny, kterou pak budeme v programu používat. Tento krok je samozřejmě značně závislý na používaném kompilátoru. Vždy je ale nutné správně uvést cesty ke vkládaným souborům.

Soubor jni.h najdeme v podadresáři include adresáře, ve kterém je nainstalován Java SDK. Kromě tohoto adresáře musíme mezi cesty uvést ještě podadresář, který odpovídá operačnímu systému, např. include\win32, apod. Například na Ubuntu Linuxu s javou 6.0 a překladačem gcc může překlad jednoduchého příkladu vypadat takhle:

gcc -fPIC \
  -I/usr/lib/jvm/java-6-sun-1.6.0.15/include/ \
  -I/usr/lib/jvm/java-6-sun-1.6.0.15/include/linux/ \
  java_Talker.c -shared -o libtalker.so

V javě pak budeme načítat knihovnu libtalker.so příkazem

System.loadLibrary("talker");

Pokud cesty uvedeme správně, po překladu získáme požadovanou knihovnu. Po spuštění dojde k nahrání knihovny a zavolání nativní funkce, která pak vypíše na konzoli příslušný text.

Předávání dat

Pro smysluplné využití JNI je nutné nativním metodám předávat parametry a získávat návratové hodnoty. Aby toto bylo možné, JNI definuje pro všechny datové typy v Javě jejich ekvivalenty, které je pak možné v nativních metodách používat. Je nutné zde rozlišit, zda se jedná o primitivní nebo objektové datové typy. S ekvivalenty primitivních datových typů lze pracovat přímo. Jejich výčet je v následující tabulce:

Datový typ v JavěNativní datový typPopis
booleanjboolean8 bitů, bez znaménka
bytejbyte8 bitů, se znaménkem
charjchar16 bitů, bez znaménka
shortjshort16 bitů, se znaménkem
intjint32 bitů, se znaménkem
longjlong64 bitů, se znaménkem
floatjfloat32 bitů
doublejdouble64 bitů

Objektové datové typy jsou předávány jako ukazatele na vnitřní datové struktury virtuálního stroje. Uspořádání těchto struktur ale zůstává nativnímu programu skryto. Jediný způsob, jak s těmito daty smysluplně manipulovat, je skrze metody, které poskytuje rozhraní JNI. Ty jsou přístupné přes ukazatel JNIEnv. Toto platí kromě běžných objektů také pro všechna pole a textové řetězce.

Příklad předávání parametrů

Dříve uvedený příklad nyní rozšíříme tak, aby nativní metoda nevypisovala na konzoli pevně daný text, ale řetězec, který dostane jako parametr. Nejprve proto upravíme deklaraci metody v Javě:

public class Talker {
    private native void sayIt(String text);
  
    public static void main(String[] args) {
        System.loadLibrary("library");
        new Talker().sayIt("Saying 'Hello World!' is boring...");
    }
}

Opět je nutné třídu přeložit a vygenerovat hlavičkový soubor s příslušnou signaturou dané metody. Ta bude v tomto případě vypadat takto:

JNIEXPORT void JNICALL Java_Talker_sayIt
  (JNIEnv *, jobject, jstring);

Nyní je nutné upravit také implementaci nativní metody:

#include <jni.h>
#include <stdio.h>
#include "Talker.h"

JNIEXPORT void JNICALL
Java_Talker_sayIt(JNIEnv *env, jobject obj, jstring text) {
    const char *str;
    str = env->GetStringUTFChars(text, NULL);

    if (str == NULL) {
        return; /* Chyba při alokaci paměti */
    }

    printf("%s", str);
    env->ReleaseStringUTFChars(text, str);
    return;
}

Reference na původní objekt se nejprve převede na ukazatel na sekvenci znaků. Vzhledem k tomu, že pro tuto operaci je nutné alokovat paměť, je potřeba zkontrolovat, zda operace byla úspěšná. Poté se vypíše daný text na konzoli a alokovaná paměť se opět uvolní.

Odkazy

Literatura

  • LIANG, Sheng. The Java Native Interface : Programmer's Guide and Specification [PDF online]. Addison-Wesley, 1999-06, rev. 2002-02-21 [cit. 2009-05-17]. [java.sun.com/docs/books/jni/download/jni.pdf Dostupné online]. ISBN 0-201-32577-2. 
  • TAL, Liron. Enhance your Java application with Java Native Interface (JNI). JavaWorld. 20. říjen 1999. Dostupné v archivu pořízeném dne 2009-06-09.  Archivováno 9. 6. 2009 na Wayback Machine.

Reference

Související články

Externí odkazy