JNIOggi parliamo di JNI.

JNI, acronimo di Java Native Interface, è una delle tante tecnologie messe a disposizione da Java. In particolare, essa permette agli sviluppatori Java di interfacciarsi con funzioni di libreria scritte in codice nativo. Cosa significa questa cosa: significa che è possibile sfruttare delle librerie scritte appositamente per una piattaforma, anche all’interno di programmi scritti in Java, che, per sua natura, è cross-platform.

Cercando di essere ancora più chiari, tramite questa tecnologia è possibile far fare a Java delle cose che, altrimenti, non avrebbe potuto fare, visto che sono cose legate strettamente ad un particolare sistema operativo.


Ma vediamo dove e quando è necessario utilizzare questa tecnologia: come già detto, Java è cross-platform. Se da un lato questa caratteristica è stata una delle principali forze trainanti che hanno contribuito al diffondersi del linguaggio, essa è stata anche la causa di diversi malcontenti. Per rendere cross-platform un linguaggio, infatti, è necessario dotarlo di alcune astrazioni. Parte di queste astrazioni hanno consentito agli sviluppatori Java di ottenere dei miglioramenti nella programmazione, velocizzandone la stesura del codice e liberando il programmatore da aspetti non strettamente legati all’applicazione (ad esempio, la gestione dei File e degli Stream). Altre, invece, hanno reso più difficile o addirittura impossibilitato la gestione di particolari situazioni (ad esempio, la gestione dei segnali).

Per poter continuare a mantenere la portabilità, accontentando allo stesso tempo coloro che vorrebbero poter utilizzare alcune peculiarità del sistema operativo su cui sviluppano, si è giunti ad un compromesso: la tecnologia JNI. Essa è una libreria che fornisce allo sviluppatore un’interfaccia verso altre librerie, che sono in codice nativo. Questo, d’altro canto, significa che l’applicazione che verrà scritta utilizzando tale tecnologia potrebbe non essere più cross-platform e non potrà più valersi della dicitura “100% Pure Java“. Perchè dico “potrebbe”? Perchè nessuno vieta di sviluppare librerie native per più di ua piattaforma e allegare queste all’applicazione. Se il nostro obiettivo, ad esempio, è scrivere un’applicazione Java che giri sia sotto Windows, che sotto Linux e che sfrutti di entrambi alcune peculiarità, potremo generare due librerie native di tipo diverso ed allegarle entrambe alla nostra applicazione Java, senza toccare nemmeno una virgola del codice Java originario.

Ma veniamo al sodo e diamo un’occhiata ad un piccolo esempio. Supponiamo di voler sviluppare un’applicazione Java che effettui la somma di due interi presi da linea di comando. Siccome non ci accontentiamo facilmente, vogliamo che la somma venga effettuata da una routine scritta in C++ e compilata in codice nativo, in modo da poter usufruire delle (eventuali) agevolazioni messe a disposizione dal runtime C++ per il sistema operativo ospite. In particolare, vogliamo che questa routine sia contenuta all’interno di una DLL Windows. Questi sono i passi che dovremo compiere:

  1. Scrivere il programma Java, utilizzando JNI per indicare che la funzione somma() si trova in una DLL
  2. Compilare il programma
  3. Ottenere un header (file .h) che mi rappresenti l’interfaccia della funzione somma()
  4. Scrivere la DLL, tenendo conto dell’interfaccia e compilarla

Effettuiamo, quindi, queste operazioni passo per passo. Cominciamo con il punto 1 e scriviamo un programma Java che utilizzi una funzione somma() contenuta in una libreria, che chiameremo “mylib“. Da notare che il compilatore Java non sa nulla dell’esistenza o meno di questa libreria.

public class SommaNumeri {
   public native int somma(int a, int b);
   public static void main(String[] args) {
      SommaNumeri sn = new SommaNumeri();
      int a = Integer.parseInt(args[0]);
      int b = Integer.parseInt(args[1]);
      System.out.println( sn.somma(a, b) );
   }
   static {
      System.loadLibrary(”mylib”);
   }
}
 

Possiamo notare alcune cose interessanti. Per prima cosa il metodo somma() è solo dichiarato, ma non implementato. Questo è possibile poichè esso è stato dichiarato utilizzando il modificatore native che indica al compilatore di non cercare l’implementazione concreta di questo metodo, poichè esso sarà contenuto in una libreria esterna. Al compilatore dobbiamo solo comunicare la firma del metodo, in modo da consentirgli di effettuare i controlli minimi di correttezza sintattica e semantica. Altra cosa importante è il blocco static che c’è alla fine del codice d’esempio: esso serve per caricare la libreria esterna. Ultima cosa importante è l’assenza dell’estensione nel nome della libreria (nel nostro caso sarà mylib.dll): in questo modo è possibile rendere “portabile” l’applicazione Java, lasciando al Runtime il compito di reperire la libreria corretta, che potrebbe avere nomi diversi (.dll sotto Windows, .so sotto Linux, ecc). Tra le cose non interessanti, per questo articolo, c’è la totale assenza della gestione di eventuali eccezioni: nell’esempio non mi sono preoccupato di gestire eventuali anomalie nei dati d’ingresso, non essendo argomento di questa trattazione.

A questo punto non ci resta che compilare l’applicazione ed ottenere il relativo .class:

javac SommaInteri.java

Ora come ora abbiamo ottenuto un file .class, ma esso non è ancora utilizzabile: se provassimo ad avviarlo otterremmo l’eccezione “UnsatisfiedLinkException”, poichè la JVM non riuscirebbe a caricare la libreria mylib.dll, come indicato nel blocco static.

Fatto questo, dobbiamo ottenere il file header per la DLL. Per poterlo ottenere ci dobbiamo appoggiare ad un tool fornito assieme alla JDK: javah. Questo tool analizza il file class passato come argomento e restituisce un file .h contenente l’intestazione delle funzioni che dovremo scrivere. Questo rappresenta l’interfaccia tra Java ed il codice nativo. Procediamo, dunque, con questo comando:

javah -jni SommaInteri

Notiamo che anche in questo caso, come per l’esecuzione, non va specificata l’estensione della classe. Come per l’esecuzione, quindi, dobbiamo fare attenzione al posizionamento in caso di utilizzo della clausola package. Dopo l’esecuzione di questo comando, viene generato un file .h con lo stesso nome della classe (eventualmente preceduto dall’intestazione del package in cui il punto viene sostituito dal carattere “_”). Il file prodotto dovrebbe essere questo:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class SommaNumeri */

#ifndef _Included_SommaNumeri
#define _Included_SommaNumeri
#ifdef __cplusplus
extern “C” {
#endif
/*
 * Class:     SommaNumeri
 * Method:    somma
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_SommaNumeri_somma
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

Questo file è molto interessante perchè ci fa capire come vengono mappati in Java i metodi nativi: al prefisso Java viene concatenato il nome completo della classe, seguito dal nome del metodo. Il tipo del valore di ritorno e quelli dei parametri, inoltre, sono un po’ particolari. Essi, infatti, sono delle ridefinizioni di tipi, che permettono a Java di interfacciarsi con gli altri linguaggi. Vediamo un po’ più nel dettaglio quali sono i parametri che la nostra funzione dovrà prevedere:

  • Il primo è un puntatore all’ambiente (environment): esso fornisce tutta una serie di metodi per il trattamento di oggetti che si interfacciano con Java. Ne vedremo un utilzzo più avanti, nella scrittura della funzione.
  • Il secondo è un puntatore alla classe che richiama la funzione (il famoso “this”).
  • Infine c’è la lista dei parametri effettivamente passati da Java, ovvero i nostri due interi.

Uno sguardo più attento ci fa notare, tra i commenti, la firma del metodo (chiamata Signature): essa viene costruita utilizzando una nomenclatura particolare che possiamo trovare nella documentazione di JNI. Nel caso specifico, ci sono due “I” fra parentesi, seguite da un’altra “I”: significa che la funzione prende due interi e ritorna un intero.

Ora che abbiamo ottenuto il nostro file header, possiamo dedicarci alla scrittura della funzione in C++ e alla sua compilazione per ottenere la DLL. Dovremo, quindi, includere il file di intestazione nel sorgente del corpo della funzione e scrivere l’effettiva implementazione della nostra somma:

#include <windows.h>
#include “SommaNumeri.h”

JNIEXPORT jint JNICALL Java_SommaNumeri_somma
         (JNIEnv *env, jobject obj, jint a, jint b) {

   return a + b;
}

BOOL APIENTRY DllMain (
  HINSTANCE hInst  /* Library instance handle.     */ ,
  DWORD reason     /* Reason this is being called. */ ,
  LPVOID reserved  /* Not used. */ ) {

    switch (reason)
    {
      case DLL_PROCESS_ATTACH:
        break;

      case DLL_PROCESS_DETACH:
        break;

      case DLL_THREAD_ATTACH:
        break;

      case DLL_THREAD_DETACH:
        break;
    }

    /* Returns TRUE on success, FALSE on failure */
    return TRUE;
}

Compilando, ora, il codice C++ dovremmo ottenere la nostra DLL, assieme ad una serie di altri file, che non ci interessano.

A questo punto abbiamo finito. Abbiamo generato la DLL, che il nostro programma Java andrà ad utilizzare. Essa dovrà essere posizionata nella stessa directory dell’applicazione oppure in una delle directory standard (%SystemRoot%\System32) in modo che il Runtime Java la possa caricare ed utilizzare.

Possiamo quindi concludere dicendo che JNI è molto semplice da utilizzare: essa fornisce un “ponte” fra il codice Java e quello nativo, consentendo quindi di eliminare le limitazioni imposte per mantenere il codice cross-platform. In aggiunta va tenuto presente che JNI non è un modo per richiamare direttamente funzioni scritte in codice nativo, ma solo un’interfaccia fra il codice Java e quello nativo. Buona Programmazione!

Tags: