Java la viteză maximă

Înglobarea funcțiilor native într-un program Java permite obținerea unei viteze de execuție maxime.

Java, acest copil minune al lumii IT, are neajunsul că programele scrise în acest limbaj vor rula mai încet decât programele compilate într-un cod binar, corespunzător platformei pe care se va executa programul. Totuși, pentru a da posibilitatea realizării unor programe, care trebuie să rezolve anumite sarcini într-un timp limitat, proiectanții limbajului Java au introdus posibilitatea de a apela funcții externe, compilate în codul specific platformei pe care se lucrează.

Astfel, un program Java poate apela funcții dintr-o bibliotecă de funcții (un fișier .DLL sub Windows sau .so în cazul diferitelor implementări de UNIX). Este important de remarcat faptul că, datorită diferitelor implementări ale bibliotecilor de funcții, precum și diferențelor de cod binar dintre diferitele platforme, se pierde portabilitatea codului Java obținut. De aceea, este important ca utilizarea metodelor native să se facă doar în cazurile în care viteza execuției este critică.

Includerea unor funcții native în codul Java se face într-un proces format din 5 pași.

1.Crearea sursei Java

În acest pas se declară toate metodele, atât cele native, cât și cele Java. Când se folosește o metodă scrisă, într-un alt limbaj decât Java, trebuie să includeți cuvântul cheie native în definiția metodei. Acesta va spune compilatorului că este vorba despre o funcție (metodă) nativă. Astfel, pentru exemplificare, am realizat un program care sortează elementele unui vector folosind algoritmul BubbleSort. Clasa Java care definește metoda nativă arată în felul următor:

class functie {
 	  public native void sortc(int []vect);
 	  static {
 	  System.loadLibrary("functie"); }}

În această clasă, definirea metodei va crea doar semnătura acesteia, implementarea urmând să fie făcută într-un alt limbaj de programare. Folosirea cuvântului cheie native specifică compilatorului (javac) că implementarea funcției trebuie căutată într-o bibliotecă de funcții, care se încarcă cu metoda loadLibrary a clasei System. În cazul nostru, biblioteca de funcții se va numi functie.dll dacă lucrăm în Windows, libfuncție.so în diferite implementări de UNIX.

În continuare, trebuie creat programul (aplicația), care va utiliza această metodă nativă. Acest program va crea o instanță a clasei anterior definite (în cazul nostru: funcție) și va apela metoda nativă definită în aceasta, așa cum se vede din exemplul următor:

public static void main(String args[]) {
 	  . . .
 	  new functie().sortc(arr);
 	  . . .
 	  } 	 

Se poate observa din exemplul de mai sus că o metodă nativă se apelează ca și o metodă Java obișnuită.

2.Compilarea surselor Java

În acest pas, se creează fișierele .class asociate celor două surse Java. De observat că putem compila și aplicația care folosește metoda nativă, cu toate că aceasta încă nu a fost implementată.

3.Crearea fișierului antet

În acest pas, se folosește utilitarul javah pentru a crea fișierul antet (functie.h) din clasa functie.class javah –jni functie.

Fișierul antet obținut va conține prototipul funcției. Dacă ne uităm în fișierul functie.h creat, vom găsi declarația funcției în felul următor: JNIEXPORT void JNICALL Java_functie_sortc (JNIEnv*, jobject, jintArray). În această declarație, numele funcției se compune din prefixul Java_, numele pachetului, numele clasei și numele metodei native separate de semnul „_“. În cazul nostru numele pachetului nu apare, deoarece clasa se află în pachetul implicit și, astfel, numele funcției devine Java_functie_sortc. De observat că, pe lângă parametrul tip vector de întregi care s-a transmis funcției, aceasta mai primește doi parametri: JNIEnv * și job ject. Primul parametru reprezintă o interfață prin intermediul căreia funcția poate accesa parametri și obiectele pe care i le dăm ca argument. Al doilea parametru este o referință la obiectul însuși, în care este declarată metoda nativă.

4. Implementarea metodei native

În acest pas se efectuează implementarea propriu zisă a metodei (funcției) într-un limbaj de programare nativ.

Funcția creată trebuie să aibă aceeași semnătură, ca cea generată cu javah în fișierul funcție.h:

JNIEXPORT void JNICALL Java_func tie_sortc
 	  (JNIEnv *env, jobject obj, jintArray x) 

Implementarea funcției este prezentată mai sus. Acest cod include două fișiere antet:
• jni.h care furnizează informațiile necesare codului nativ pentru a putea interacționa cu mediul de execuție Java.
• functie.h care a fost generat în etapa 3 și care conține prototipul funcției native.

5.Crearea bibliotecii de funcții

După implementarea funcției native, pentru a putea fi utilizată de către programul Java, trebuie să creăm o bibliotecă de funcții care să conțină această funcție. Această bibliotecă se obține prin compilarea fișierului sursă în care este implementată funcția nativă:

Sub UNIX folosim cc –shared -I/usr/local/ java/include -I/usr/local/java/include/solaris functie_imp.c -o libhello.so

Sub Windows95 putem folosi: cl /LD -Ic:\jdk1.1.4\include -Ic:\jdk1.1.4\include\ win32 functie_imp.c -Fefunctie.dll

După efectuarea cu succes a acestor operații trebuie să aveți toate fișierele necesare pentru a încerca rularea unui program Java care folosește cod nativ.

La realizarea unor metode, de obicei, suntem nevoiți să transferăm anumiți parametri acestora. Pentru transferul de parametri dintre Java și metodele native apelate, se folosește interfața JNI (Java Native Interface). Această interfață oferă funcții standard care permit metodelor native să acceseze și manipuleze obiectele Java, să creeze noi obiecte Java, să apeleze metodele unor obiecte și altele.

Schimbul de parametri dintre Java și codul nativ

O metodă nativă poate accesa în mod direct tipurile de date primitive din Java, care sunt transferate ca parametri. Astfel, tipurile primitive din Java sunt mapate în tipuri native, după cum arată următorul tabel.

Transferul obiectelor se face prin referință. Toate referințele obiectelor Java au tipul jobject, dar pentru a înlesni munca programatorilor s-au introdus diferite „subtipuri“ ale tipului jobject:
• jclass - corespunde obiectului java.lang .class
• jstring - corespunde obiectului java.lang .String
• jarray - reprezintă matricele și are subtipuri pentru matrice de obiecte (jobjectArray), de tipuri numerice (jbooleanArray, jbyteAr ray, jcharArray, jshortArray, jintArray, jlongArray, jfloatArray, jdoubleArray).
• jthrowable - este reprezentarea nativă a obiectului java.lang.Throwable

Pentru accesul la variabilele sau metodele mediului de execuție, Java se folosește pointerul JNIEnv * care reprezintă o interfață între mediul de execuție Java și metoda nativă. Astfel, în exemplul prezentat am folosit pointerul env, pentru a accesa elementele matricei x transferate ca parametru. Prin intermediul acestui pointer, am putut apela diferite metode referitoare la matricea x, cum ar fi aflarea dimensiunii acesteia, sau accesul la valorile stocate.

În exemplul prezentat, am implementat metoda de sortare „BubbleSort“, atât în Java cât și în C pentru a putea vedea diferența de viteză între execuția aceleiași operații. Mai întâi se lansează metoda de sortare implementată în Java, apoi cea în C. Programul se lansează în execuție cu comanda java Sort Item și se mai pot preciza doi parametri opționali. Primul este „off“, care specifică programului să nu afișeze valorile elementelor matricelor cu care lucrează, iar al doilea poate fi un număr întreg pozitiv, care să indice numărul de elemente conținute în matrice.

Referințe

1. Jerry R. Jackson, Allan L. McClellan: Java by example, Prentice Hall
2. Michael Morrison: Java Unleashed, Sams Publishing
3. The Java Tutorial: http://java.sun.com/ docs/books/tutorial/native1.1/index.htm


BYTE România - decembrie 1997


(C) Copyright Computer Press Agora