Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Java JNI

Introduzione a Java Native Interface, che permette l'utilizzo di risorse del sistema operativo per assolvere a compiti di bassissimo livello
Introduzione a Java Native Interface, che permette l'utilizzo di risorse del sistema operativo per assolvere a compiti di bassissimo livello
Link copiato negli appunti

La forza del linguaggio di programmazione Java risiede nel suo motto: "write once, run everywhere". La portabilità del codice permette di avere uno strumento facile da utilizzare e concettualmente portabile, ma alle volte troppo astratto per risolvere problemi di basso livello e sicuramente meno performante in quanto eseguito da una macchina virtuale e non direttamente da un processo di sistema operativo.

Lasciamo la discussione sulle performance, ma per quanto riguarda l'utilizzo di funzioni di basso livello non sarà mai possibile raggiungere gli standard di linguaggi come il C o il C++. Fortunatamente però esiste l'interfaccia JNI (Java Native Interface) che permette l'utilizzo di risorse del sistema operativo per assolvere a compiti di bassissimo livello, che altrimenti l'alta astrazione di Java non consentirebbe di svolgere.

Qualche anno fa, quando ancora i servizi di invio SMS non erano così evoluti, mi sono imbattuto in un'applicazione Java che aveva la necessità di comunicare con un apparecchio GSM, una specie di telefonino, per la gestione di una campagna promozionale via SMS. Il dispositivo era collegato con la porta seriale del server. Tutto ciò che c'era a disposizione era una DLL (Dynamic Link Library), che era l'unico modo per interrogare il dispositivo. Quindi il problema da risolvere era poter far comunicare l'applicazione Java (e quindi la lista degli utenti del sistema) con il dispositivo (e quindi inviare i messaggi SMS).

Una DLL di fatto è un driver, o un estensione del sistema operativo, che guida un determinato componente (generalmente hardware) utilizzando uno scambio di informazioni a basso livello (quindi scrittura su un buffer condiviso di IO) per gestire le sue funzioni di input/output. Java, viene eseguito in un suo processo (dalla Virtual Machine) e non avrebbe alcun modo di comunicare con altri processi (e spazi di memoria) a meno di usare JNI. Questo è un meccanismo utilizzato a tempo di compilazione per consentire l'utilizzo di servizi nativi del sistema operativo, siano essi processi in esecuzione o librerie da caricare dinamicamente (DLL).

Esempio pratico

Il nostro esempio ci mostrerà come poter interrogare una DLL. Come esempio pratico non andremo a scrivere un driver, ma utilizzeremo il linguaggio C per scrivere una semplice routine che utilizzi le risorse del sistema operativo Win32, in questo caso l'utilizzo di una finestra di informazioni (un MessageBox di Windows).

È opportuno chiarire a questo punto che utilizzando risorse native, si perderà la portabilità, quindi, il programma che scriveremo girerà su macchine Windows, ma non Linux, a meno di scrivere una routine di sistema anche per quest'ultimo sistema operativo o di collegare l'opportuna libreria.

Procediamo per ordine e introduciamo le operazioni che dovremo eseguire, valide nel caso generale:

  1. Scrivere la classe Java wrapper verso il servizio nativo;
  2. Compilare la classe;
  3. Estrarre l'interfaccia nativa dalla classe compilata;
  4. Scrivere la routine estendendo l'interfaccia generata;
  5. Compilare la libreria DLL.

Come linguaggio per scrivere la libreria (punti 4 e 5) utilizzeremo C (in questo modo potremo utilizzare gli stessi sorgenti e compilare su altri sistemi operativi per ottenere diverse librerie).

Utilizzando un linguaggio di programmazione ad oggetti è sempre bene idealizzare il servizio come un entità, un oggetto ben definito. Creeremo quindi un wrapper che espone i metodi nativi che vogliamo realizzare:

Listato 1. Espone i metodi nativi

package it.html.jni;

public class HelloWorldConsoleWrapper {
  //Metodo nativo (recupereremo l'implementazione dalla libreria
  public native void printf(String line);
  
  //Definiamo il nome della libreria da utilizzare
  static{
    System.loadLibrary("ProgettoJNI");
  }
}

Chiamiamo il metodo printf, per rifarci al printf del C (che riutilizzeremo come sistema di log).

Il modificatore di accesso native, spiega al compilatore che tale metodo verrà caricato a runtime utilizzando una libreria nativa esterna, mentre nel blocco static definiamo propriamente la libreria (senza estensione e che dovrà trovarsi sotto il classpath) che poi chiameremo "ProgettoJNI.dll".

Compiliamo la classe, in maniera tradizionale, facendo attenzione a ricordare il path sotto cui verrà compilata la classe (it.html.jni).

Abbiamo la classe, ora esportiamo l'interfaccia, posizionandoci con una console dei comandi alla radice del path, quindi eseguiamo:

javah it.html.jni.HelloWorldConsoleWrapper

Dove javah è un comando che risiede nel path JAVA_HOME/bin.

Il risultato è la produzione della seguente interfaccia .h (in paradigma procedurale, l'interfaccia ha lo stesso concetto di astrazione di un paradigma ad oggetti):

Listato 2. Interfaccia nativa dalla classe compilata

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

#ifndef _Included_it_html_jni_HelloWorldConsoleWrapper
#define _Included_it_html_jni_HelloWorldConsoleWrapper
#ifdef __cplusplus
extern "C" {
  #endif
  /*
   * Class: it_html_jni_HelloWorldConsoleWrapper
   * Method: printf
   * Signature: (Ljava/lang/String;)V
   */

  JNIEXPORT void JNICALL Java_it_html_jni_HelloWorldConsoleWrapper_printf (JNIEnv *, jobject, jstring);
  
  #ifdef __cplusplus
}
#endif
#endif

Viene creata la definizione di un metodo Java_it_html_jni_HelloWorldConsoleWrapper_printf che si aspetterà un puntatore all'ambiente di esecuzione, l'oggetto e il parametro del metodo. Non preoccupatevi se il concetto non è molto chiaro, si tratta semplicemente di una interfaccia che utilizzeremo per scrivere la routine C nel seguente punto.

A partire dall'interfaccia del punto 3, scriviamo la routine C (il nome del file può essere qualsiasi, per noi è dllmain.c):

Listato 3. Estendendo l'interfaccia generata creiamo la routine

#include "it_html_jni_HelloWorldConsoleWrapper.h"
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

JNIEXPORT void JNICALL Java_it_html_jni_HelloWorldConsoleWrapper_printf(JNIEnv *env, jobject obj, jstring a){
  MessageBox (0, "Hello World!n", "Hi", MB_ICONINFORMATION);
  printf("%s",a);
}

Includiamo l'interfaccia generata e definiamo il corpo del metodo. Nel nostro esempio ci preoccupiamo di richiamare una funzione di sistema per aprire un box (MessageBox) e di una per scrivere in output (printf). È evidente che questo sarà il vostro punto di accesso per l'utilizzo o la creazione di qualsiasi strumento a livello di sistema operativo (file, processi, pipe, socket, ...).

Ora possiamo compilare con un compilatore standard (io ho utilizzato l'IDE Dev C++) non dimenticando di includere il file di interfaccia generato (.h) e tutte le interfacce presenti in JAVA_HOME/include e JAVA_HOME/include/win32.

Il risultato sarà la generazione di una DLL che chiamiamo ProgettoJNI.dll, come avevamo previsto. Includendo la DLL a livello di classpath siamo pronti ad eseguire un'istanza della classe HelloWorldConsoleWrapper:

Listato 4. Utilizziamo un main per testare la classe wrapper

public class HelloJNIMain{
  public static void main(String[] args){
    ..//
    try{
      HelloWorldConsoleWrapper console = new HelloWorldConsoleWrapper();
      console.printf("Salve "+args[0]+", benvenuto.");
    }catch(java.lang.UnsatisfiedLinkError e){
      //Questa eccezione si presenta nel caso non si riesca a leggere
      //la libreria nativa, quindi la gestiamo.

      System.out.println("Errore nella lettura della libreria.");
    }
  }
}

Dopo aver creato un'istanza della classe, richiamiamo il metodo passando un semplice parametro.

Esecuzione e altri esempi

Figura 1. Esecuzione Main
Esecuzione Main

Il semplice esempio è servito per mostrare come sia possibile utilizzare l'interfaccia JNI per servirsi delle risorse di basso livello del sistema operativo. Nell'introduzione è stato citato un esempio concreto del suo utilizzo. Effettivamente è la creazione di driver, o ancor più spesso l'utilizzo di driver già esistenti, il motivo per cui si usa l'interfaccia Native.

Se avete a che fare con un lettore di smart card, di RFID o anche il collegamento di un dispositivo mobile o di una videocamera, farete uso di JNI. Quello che è bene sapere è che l'utilizzo di JNI avviene già quando utilizzate i Thread o l'interfaccia di IO di Java o quella di Network (le Socket e tutte le classi che le estendono). A livello concreto quelle classi non fanno altro che interfacciarsi con il modificatore di accesso native a delle routine di sistema che lavorano a basso livello. Se ci pensate, una Socket ha necessità di comunicare con il driver della scheda di rete per lo scambio di informazioni, mentre uno stream su un file alla fine dovrà comunicare con l'adapter del disco rigido, quindi con il suo driver.

Come esempio vediamo come si presenta il sorgente della classe Thread:

..//
public static native Thread currentThread();
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
..//
public synchronized native void start();
..//

I metodi nativi fanno uso di implementazioni concrete a livello di sistema operativo permettendo quindi una schedulazione del multitasking anche in Java.

Ti consigliamo anche