MokaByte Numero  38  - Febbraio  2000
 
Dalle librerie 
dinamiche a Java
di 
Giuseppe Rizzo
Integrazione di vecchie e nuove tecnologie attraverso 
Java Native Interface e J/Direct

L’obiettivo di questo articolo è di introdurre un esempio che mostra come sia possibile l’integrazione di vecchi sistemi, basati su librerie dinamiche, con le nuove tecnologie Internet e Web, mettendo a confronto Java Native Interface e J/Direct.

La gestione delle eccezioni viene introdotta nella classe engine JCript. Nei metodi della classe engine JCript, dopo la chiamata al corrispondente metodo dell’interfaccia di basso livello, viene decodificato il codice d’errore attraverso CriptException.solve(int) e, se tale codice è diverso da zero, viene lanciata un’eccezione di tipo CriptException.
Per quanto riguarda il valore di ritorno, nei metodi della classe engine, prima della invocazione del metodo corrispondente nell’interfaccia di basso livello, si istanzia l’oggetto da ritornare al chiamante, si effettua la chiamata all’interfaccia di basso livello passando tale oggetto come parametro (semantica C) e si restituisce al chiamante l’oggetto come valore di ritorno (semantica Object Oriented).
Da notare che per poter compiere questa trasformazione semantica, occorre avere sempre un unico oggetto come valore di ritorno, di conseguenza:
  • nell’API C il risultato può essere composto da più variabili passate come parametro alle funzioni;
  • nell’API Java il risultato è un unico oggetto. 
Prendendo come esempio la funzione:
int verify(BUFFER* inBuf, BUFFER* outBuf, int* algorithm, SET_DN* set_dn);
si nota che il risultato è composto da buffer di output ( outBuf), algoritmo (algorithm) e insieme di firmatari (set_dn). Il corrispondente metodo Java è:

         public VerifyResult verify(JBuffer inBuf) throws CriptException;

dove VerifyResult contiene il buffer di output,  l’algoritmo e l’insieme dei firmatari.
Di seguito è riportato il codice relativo all’interfaccia di alto livello, dovuto alla trasformazione da semantica C a semantica ad oggetti delle funzioni pubbliche dell’API C cript_api.
 
 

 package gr.cript;

public interface JCriptHighLevelInterface {
  public void changePassword(String oldPwd, String newPwd) throws CriptException;
  public DecryptResult decrypt(String pwd, JBuffer inBuf) throws CriptException;
  public JBuffer encrypt(Algorithm alg, JDn dn, JBuffer inBuf) throws CriptException;
  public JDn login(String pwd) throws CriptException;
  public JBuffer sign(Algorithm alg, String pwd, JBuffer inBuf) throws CriptException;
  public VerifyResult verify(JBuffer inBuf) throws CriptException;
}

Codice 2: API Java di alto livello 

Altri aspetti interessanti della classe engine JCript sono:

  • Il costruttore public JCript(Applet ap), che riconosce il contesto nel quale l’applet viene eseguita e istanzia il provider opportuno JniCriptStandard.java, JniCriptNetscape.java, JDirectCript.java. (La creazione di due implementazioni del provider JNI è dovuta alla diversa gestione della sicurazza nell’esecuzione della applet implementata da Netscape rispetto allo standard Sun, e di conseguenza alla diversità nell’accesso alle librerie dinamiche; essi sono estensioni del provider JNI vero e proprio: JniCript.java).
  • L’integrazione con il browser. Nel metodo getBrowserCode(Applet ap) si evidenzia come, attraverso la tecnologia Netscape LiveConnect [LIVECONN] sia possibile accedere alla gerarchia di oggetti del browser e di conseguenza al nome del browser stesso.


Il codice della classe engine JCript è contenuto nel file JCript.java.
In questa prima parte sono state affrontate alcune delle problematiche relative alla trasformazione di applicazioni nate in un contesto diverso da quello del Web.
In particolare si è visto come trasformare un’API C in un’API Java, costruendo una struttura ad oggetti attorno ad una libreria non object-oriented, e come costruire una architettura che consenta di superare i problemi legati alla diversità delle piattaforme al fine di implementare codice portabile e riusabile.
Nei paragrafi successivi viene descritto come implementare i providers JniCript e JDirectCript ovvero le classi Java e C che consentono l'effettivo accesso alla libreria dinamica Cript_api.
 
 
 

Implementazione di codice JNI e J/Direct: Classi Provider 
Con il termine "provider" si intende un generico sistema di "basso livello", in grado di fornire dei servizi. Nel caso specifico si tratta di un sistema che implementa l'accesso ad una libreria dinamica da codice Java. Esso si colloca ad un livello intermedio tra API Java (implementata dalla classe engine) e API C, come mostrato in Figura 5. 
La presenza di questo livello intermedio consente,  come visto in precedenza di rendere trasparente l'accesso alla libreria dinamica, indipendentemente dalla piattaforma (browser + sistema operativo).
Nell'esempio proposto sono presenti due provider: uno implementato con tecnologia JNI, l'atro implementato con J/Direct.
 
 
 

Provider JNI

Il provider JNI è formato da due livelli (Figura 5) il primo scritto in Java contenente:

  • la classe JniCript.java che si occupa della dichiarazione dei metodi nativi 
  • le classi JniCriptStandard.java  e JniCriptNetscape.java che si occupano del caricamento della libreria dinamica nativa JniCript 
il secondo scritto in C con sintassi JNI contenente:
  • il file header gr_cript_jni_JniCript.h con i prototipi delle funzioni native 
  • il file JniCript.c contenente l'implementazione C con sintassi JNI dei metodi nativi 
E' all'interno del codice C nel file JniCript.c che si trovano le chiamate alla libreria dinamica Cript_api. Il file JniCript.c è diviso nelle seguenti sezioni:
  • Inizializzazione degli identificatori di classi e metodi Java 
  • Implementazione dei metodi nativi (interfaccia JCriptLowLevelInterface) 
  • Conversione di oggetti Java in C (conversione parametri di input) 
  • Conversione di strutture C in Java (conversione parametri di output) 
Ogni funzione della sezione "Implementazione dei metodi nativi" e a sua volta composta da:
  • Inizializzazione e conversione dei parametri di input e allocazione parametri di output 
  • Chiamata alla corrispondente funzione esportata dalla libreria dinamica 
  • Conversione dei parametri di output e ritorno al chiamante (C -> Java) 
Per meglio comprendere la struttura del Provider JNI analizziamo come avviene l'accesso ad una generica funzione della libreria dinamica Cript_api del tipo:
 
int verify(BUFFER* inBuf, BUFFER outBuf, int* algorithm, SET_DN* dn);


dove inBuf è il parametro di input mentre outBuf algorithm e signers sono i parametri di output. Il metodo nativo corrispondente è:

         public native int verify(JBuffer inBuf, gr.cript.VerifyResult res);

dove la truttura BUFFER è mappata in JBuffer mentre l'output composto da output buffer + algorithm + signers è accorpato in un'unico oggetto VerifyResult. Il codice della implementazione della funzione nativa è il seguente:

 /*
 * Class: gr_cript_jni_JniCript
 * Method: verify
 * Signature: (Lgr/cript/JBuffer;Lgr/cript/VerifyResut;)I
 */

JNIEXPORT jint JNICALL Java_gr_cript_jni_JniCript_verify
(JNIEnv *env, jobject thisObj, jobject inBuf, jobject stateBuf) {
1    int alg,res;
2    SET_DN* signers;
3    init_class(env);
4    init_method(env);
5    in_buf = BUFFER_new();
6    JBufferToBUFFERPtr(env,inBuf,in_buf);
7    signers = (SET_DN*)malloc(sizeof(SET_DN));
8    signers->length = 0;
9    out_buf = BUFFER_new();
10   res = verify(in_buf,out_buf,&alg,signers);
11   if (res == 0)
12     decodeResultToVerifyResult(env,out_buf,alg,signers,stateBuf);
13   BUFFER_free(out_buf);
14   BUFFER_free(in_buf);
15   free(signers);
16   return res;
}

Inizializzazione degli identificatori di classi e metodi Java -> linee 3,4
Inizializzazione e conversione variabili di input -> linee 5,6
Allocazione variabili di output -> linee 7..9
Accesso alla libreria dinamica Critp_api -> linea 10
Conversione variabili di output -> linea 12
Deallacazione variabili -> linee 13..15
Ritorno al chiamante -> linea 16

Le funzioni relative ad inizializzazione degli identificatori di classi e metodi e quelle relative alla conversione dei tipi sono in JniCript.c. In seguito è riportata una breve descrizione dell'architerrura e della sintassi JNI.
Il file contenente il codice nativo C, va compilato e linkato alla libreria Cript_api, formando una libreria dinamica JniCript.dll o JniCript.so.
 
 
 

Provider J/Direct
Il provider J/Direct è del tutto equivalente al provider JNI sopra descritto, ma completamente scritto in linguaggio Java (Microsoft Java). Esso è composto da un unico file JDirectCript.java diviso nelle seguenti sezioni:

Implementazione dei metodi dell'interfaccia JCriptLowLevelInterface 
Dichiarazione delle funzioni importate dalla libreria dinamica (direttiva @dll.import) 
Conversione di oggetti Java in strutture dichiarate con la direttiva @dll.struct (conversione parametri di input) e viceversa (conversione parametri di output) 
Ogni funzione della sezione "Implementazione dei metodi del'interfaccia JCriptLowLevelInterface" e a sua volta composta da:
Inizializzazione e conversione dei parametri di input e allocazione parametri di output 
Chiamata alla corrispondente funzione esportata dalla libreria dinamica 
Conversione dei parametri di output e ritorno al chiamante 
 

public int verify(JBuffer in, VerifyResult out){
1    int inBufPtr = BUFFER_new();
2    BUFFER_set(inBuf_ptr,in.getBytes(),in.length());
3    int algPtr = DllLib.allocCoTaskMem(INTSIZE);
4    int signersPtr = DllLib.allocCoTaskMem(SET_DN_SIZE);
5    DllLib.write4(signers_ptr,0,0); 
6    int outBufPtr = BUFFER_new();
7    int res = verify(inBufPtr,outBufPtr,algPtr,signersPtr);
8    if (res==0)
9      decodeResultToVerifyResult(outBufPtr,algPtr,signersPtr,out);
10   BUFFER_free(in_buf_ptr);
11   BUFFER_free(out_buf_ptr);
12   DllLib.freeCoTaskMem(alg_ptr);
13   DllLib.freeCoTaskMem(signers_ptr);
14   return res;

Inizializzazione e conversione variabili di input -> linee 1,2
Allocazione variabili di output -> linee 3..6
Accesso alla libreria dinamica Critp_api -> linea 7
Conversione variabili di output -> linea 9
Deallacazione variabili -> linee 10..13
Ritorno al chiamante -> linea 14
 

Il codice completo del provider J/Direct è contenuto nel file JDirectCript.java.

JNI e J/Direct a confornto

La filosofia JNI (Java Native Interface) è quella di consentire al programmatore C di accedere alla JVM e al programmatore Java di usare librerie dinamiche appositamente scritte per essere chiamate da programmi  Java; RNI (Raw Native Interface) è la tecnologia corrispondente proposta da Microsoft [RNI]. Al contrario la filosofia J/Direct è quella di consentire al programmatore Java di accedere direttamente a librerie dinamiche qualsiasi, con tutto ciò che questo comporta (vedi accesso diretto alla memoria con utilizzo degli interi come puntatori).
Nel caso si vogliano utilizzare API C racchiuse in librerie dinamiche, JNI richiede la scrittura di codice C avvolgente (wrapping) interposto tra la parte Java e l'API C, quindi uno sforzo non trascurabile dovuto a:

Problemi di mapping tra i tipi di dati di questi due linguaggi. 
Problemi legati alla migrazione dei threads da un ambiente all'altro. 
Gestione della memoria (il garbage collector non può intervenire sulla parte C). 
 

Il risultato è che uno sviluppatore Java che voglia invocare funzioni C deve anche sviluppare del codice C. Anzi, deve essere un buon programmatore C visto che il tipo di operazioni richieste necessitano di una buona conoscenza degli aspetti di allocazione di memoria, conversione di tipo, gestione della concorrenza.
J/Direct e ha l'obiettivo di semplificare l'interfacciamento Java/C in ambito Windows. Bisogna ammettere che J/Direct è più immediato da usare di JNI perché non richiede la scrittura di codice C, anche se comporta una programmazione sporca e sicuramente non portabile. L'approccio di J/Direct è quello di introdurre nel codice Java delle direttive di compilazione all'interno di commenti. Le direttive vengono tradotte dal compilatore Java di Microsoft in opportune istruzioni della Virtual Machine la quale fornisce il supporto runtime per l'integrazione Java/C. Ovviamente solo il compilatore Java di Microsoft è in grado di riconoscere queste direttive e generare il codice di interfacciamento; le direttive sono racchiuse all'interno di commenti in modo che ogni compilatore non Microsoft le ignori senza sollevare errori di sintassi.
 
 
 

Java Native Interface
Il processo di integrazione di codice nativo è suddiviso in una serie di passi logici:

Dichiarazione di metodi nativi in una classe Java
Genererazione header file che contiene i prototipi delle funzioni C che implementano i metodi nativi
Implementazione di codice Nativo in C
Mapping dei tipi e delle strutture in codice Nativo 
Accesso alla JVM da codice Nativo 
Caricamento di una libreria dinamica da codice Java
 
 
 

Dichiarazione dei metodi nativi
Cominciamo col porci una domanda: come facciamo ad invocare una funzione procedurale da un linguaggio object oriented come Java? L’elemento del linguaggio più vicino al concetto di funzione sembrerebbe essere il metodo di una classe. Ed è proprio qualificando un metodo come nativo che Java si apre al mondo esterno. In pratica è possibile indicare a Java che un certo metodo è implementato altrove, in C, o comunque in un modo dipendente dalla piattaforma, qualificandolo con il modifier native nella sua dichiarazione. L’implementazione deve essere contenuta in una libreria dinamica che esporta la funzione C che lo implementa. 
Nella classe JniCript.java vengono dichiarati i metodo nativi per il Provider JNI.
 
 
 

Genererazione header file contenente i prototipi delle funzioni C che implementano i metodi nativi
Sorge spontanea una domanda: quale è il prototipo delle funzioni C che implementano i metodi nativi? Esiste un tool, javah (distribuito con il JDK) che, dato in input il codice bytecode della classe Java (contenente le dichiarazioni dei metodi nativi), genera un header file C contenente i prototipi di tutte le funzioni dichiarate native. Creare questo file è dunque molto semplice, basta compilare il file Java e lanciare il comando:

javah -jni gr.cript.jni.JniCript

Con l’opzione -jni si chiede un prototipo di funzione "stile JNI", il che significa che si vuole usare la JNI per implementare il codice nativo. Il file .h che viene generato da javah ha lo stesso nome della classe Java (con "_" al posto di "."). Il file header che si ottiene da JniCript.class è gr_cript_jni_JniCript.h. JNIEXPORT e JNICALL assicurano la compilazione del codice per Win32 che richiede keyword speciali per le funzioni esportate da una libreria dinamica. Sono delle macro dipendenti dalla piattaforma. Il nome della funzione nativa è formato da:

Java_nomepackage_nomeclasse_nomemetodo
Ciascuna funzione nativa ha come primo argomento un puntatore a JNIEnv. JNIEnv* rappresenta l’interfaccia JNI; esso è organizzato come una tabella di funzioni che possono essere invocate all’interno del codice nativo per accedere l’ambiente Java. Il secondo argomento è:
  • La classe Java di riferimento, se la funzione implementa un metodo nativo dichiarato come static 
  • L’oggetto sul quale viene invocato il metodo, se la funzione implementa un metodo di istanza.
I rimanenti argomenti rappresentano quelli dichiarati nel metodo nativo Java. E' possibile sviluppare il codice nativo sia in C che in C++. Si consiglia di seguire la strada del C++, perché la JNI fornisce una interfaccia C++ più chiara e type safety. Si osservi ad esempio la chiamata in C:
jclass cls=(*env)->FindClass(env,"java/lang/String");
L’equivalente C++ è:
jclass cls = env->FindClass ("java/lang/String");
più congruente e facile da ricordare. A questo punto si può iniziare ad implementare le funzioni native. Basta creare un file, in questo caso JniCript.c, includere Cript_api.h, copiare i prototipi delle funzioni native, e svilupparle.
 
 
 

Accesso alla JVM da codice nativo, corrispondenza dei Tipi
Rileggendo i prototipi delle funzioni native si osservano tipi come: jobject, jclass, jint. Ma cosa sono? Sono i tipi di dato nativi che corrispondono a quelli Java, e li sostituiscono nell’ambiente nativo.
La principale distinzione che bisogna fare è fra i tipi primitivi e gli oggetti. I tipi primitivi possono essere acceduti direttamente all’interno del codice nativo, al contrario degli oggetti che possono essere manipolati esclusivamente attraverso le interfacce della JNI. La tabella seguente mostra la corrispondenza tra i tipi primitivi di dato Java e quelli nativi.
 
 
Tipo Java
Tipo Nativo
Descrizione
boolean
jboolean
unsigned 1 byte
byte
jbyte
signed 1 byte
char
jchar
unsigned 2 byte
short
jshort
signed 2 byte
int
jint
signed 4 byte
long
jlong
signed 8 byte
float
jfloat
4 byte
double
jdouble
8 byte
void
void
N/A

Gli oggetti Java hanno tutti come corrispondente nell’ambiente nativo il tipo jobject. Ciò significa ad esempio che se dichiariamo un metodo nativo

public native void stampaOggetto(Classe1 oggetto);
la corrispondente funzione che lo implementa prenderà in input un jobject
JNIEXPORT void JNICALL Java_Classe1_stampaOggetto (JNIEnv*, jobject, jobject oggetto);
A parte i primi due argomenti di cui abbiamo spiegato il significato, il terzo è un jobject che corrisponde ad un oggetto della classe Java Classe1. La JNI definisce un insieme di tipi complessi concettualmente sotto tipi di jobject, allo scopo di agevolare il compito del programmatore e ridurre la possibilità di errore. 
Particolare attenzione va dedicata a jstring e jarray, infatti   jstring non è una stringa C (char*), quindi manipolarla come tale è scorretto. Tutte le operazioni su jstring devono essere compiute invocando opportune funzioni di JNIEnv*. E’ improbabile che nel codice nativo si abbia la necessità di gestire direttamente oggetti jstring; è più agevole convertire una jstring in una stringa C, elaborarla, ed eventualmente riconvertire il risultato in jstring se la funzione nativa restituisce al chiamante un tale oggetto. Quindi, le interfacce più importanti da ricordare per quanto riguarda l’oggetto jstring sono GetStringUTFChars() per la conversione da jstring a stringa C, e NewStringUTF() per la conversione da char* a jstring. Non va dimenticato di liberare la memoria riservata per il char* restituito dalla GetStringUTFChars() chiamando la ReleaseStringUTFChars().
Inoltre jarray a sua volta non è un array C e come tale non può essere acceduto se non attraverso appropriate funzioni di JNIEnv*. Questo codice ad esempio è scorretto
jintArray arr;
jint sum=0;
jsize size = env->GetArrayLength(arr);
for (int i=0; i<size; i++)
  sum += arr[i]; // è sbagliato
Ancora una volta la distinzione tra tipi primitivi di dato ed oggetti è d’obbligo. Gli array di tipi primitivi possono essere trasformati in array C invocando i metodi Get<tipo>ArrayElements() che ritornano un <tipo>*, al contrario degli altri per i quali invece è possibile soltanto l’accesso al singolo elemento attraverso i metodi Get/SetObjectArrayElement().
Alla luce di ciò, questo è il codice corretto per accedere un array di tipi primitivi da codice nativo:
jint* body = env->GetIntArrayElements (arr, 0);
for (int i=0; i<10; i++)
sum += body[i]; // è corretto 
env->ReleaseIntArrayElements(arr, body, 0);
Si noti come per ogni Get<tipo>ArrayElements() sia necessario chiamare una Release<tipo>ArrayElements() per rilasciare la memoria allocata per il valore di ritorno. Questo perché tale memoria non è automaticamente gestita dal garbage collector di Java. Onde evitare di copiare un intero array, qualora questo sia molto grande, si possono accedere regioni parziali dell’array stesso usando i metodi Get/Set<tipo>ArrayRegion().
 
 
 

Come invocare i metodi Java
Vediamo adesso come si invocano i metodi Java da codice C nativo. Per invocare un metodo su un oggetto occorre conoscere la classe di appartenenza dell’oggetto:
 

jclass clazz = env->GetObjectClass(obj);


oppure

  jclass clazz = env->FindClass(“gr/DLL/JBuffer”)
Inoltre è necessario conoscere l’identificatore del metodo:
jmethodID GetMethodID (jclass clazz, const char *name, const char *signature);
jmethodID GetStaticMethodID (jclass clazz, const char *name, const char *signature);
dove signature rappresenta la “firma” del metodo, ossia una stringa che identifica il tipo de valore di ritorno ed il numero e tipo degli argomenti; quindi, unitamente al nome, un qualcosa che identifica univocamente il metodo stesso. Anche qui fortunatamente, il JDK ci viene in aiuto fornendo il tool javap che è in grado di generare la signature dei metodi nativi di una classe Java. 
Ad esempio se la classe JDn contiene il metodo public void setDirName(String), il comando
javap -s -p JDn
produce come output
public void setDirName(java.lang.String);
/*   (Ljava/lang/String;)V   */
L’opzione -s informa javap che vogliamo le signature, l’opzione -p include anche i membri privati della classe. Quindi per ottenere l’identificatore del metodo setDirName:
  jmethodID mID = GetMethodID (clazz, "serDirName", "(Ljava/lang/String;)V");
Tutte queste funzioni ritornano un jmethodID oppure 0 in caso di errore. Il passo finale consiste nella chiamata del metodo usando una delle funzioni dell’interfaccia JNI
 
jobject CallObjectMethod (jobject obj, jmethodID  methodID, ...);
jobject CallStaticObjectMethod (jclass clazz, jmethodID methodID, ...);
Come si vede, sono funzioni con un numero variabile di argomenti. I primi due sono facili da intuire, gli altri verranno passati al metodo Java da chiamare e devono rispettarne la signature.
 
 
 

Come accedere alle variabili Java
La sequenza di operazioni da eseguire per accedere i membri Java è identica a quella descritta per l’invocazione dei metodi: bisogna prima ottenere un oggetto che rappresenta la classe nell’ambiente nativo, poi generare la signature del membro, quindi chiamare una delle funzioni JNI:
 

jfieldID GetFieldID (jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID (jclass clazz,const char *name, const char *sig);


per ottenere l’identificatore del membro, quindi

jobject GetObjectField (jobject obj, jfieldID fieldID);
jobject GetStaticObjectField (jobject obj, jfieldID fieldID);


per ottenere il valore del membro, oppure

void SetObjectField (jclass clazz, jfieldID fieldID, jobject value);
void SetStaticObjectField (jclass clazz, jfieldID fieldID, jobject value);


per modificarne il valore. JNI definisce funzioni più specifiche per i tipi primitivi. Ad esempio, se il membro è di tipo intero useremo Get/SetIntField, e via discorrendo per gli altri tipi primitivi.
 
 
 

Caricamento della libreria nativa  nel codice Java
Compilata la libreria che esporta le funzioni native C, bisogna stabilire un collegamento con la classe Java. Da qualche parte nel codice Java dovrà esserci una istruzione che dice dove andare a cercare i metodi dichiarati nativi. Ebbene, la classe java.lang.System definisce il metodo statico loadLibrary() per poter caricare codice di una libreria dinamica nativa.

static {
    System.load ("JniCript");
}
Il runtime di Java caricherà JniCript.dll o JniCript.so in fase di inizializzazione. In caso di problemi il
linker di Java lancerà l’eccezione UnsatisfiedLinkException. 
 

J/Direct
Gli aspetti principali da analizzare sono i seguenti:

  • Direttive per il compilatore
  • Mapping dei tipi e delle strutture C in Java
  • Puntatori e Accesso diretto alla memoria da codice Java


Direttive per il compilatore
Ecco un esempio di accesso ad una libreria dinamica attraverso J/Direct:

public class JdExample implements JExampleLowLevelInterface {
...
// Metodo dell’interfaccia JExampleLowLevelInterface
  public int chkSmart() { 
    return checkSmart();
  }
 ...

  // Funzione della libreria Example_api
  /** @dll.import("Example_api") */
  private static native int checkSmart();
    ...
  }


La prima linea, racchiusa tra commenti (/** e */), è la direttiva J/Direct. Nell'esempio la direttiva indica al compilatore che il metodo checkSmart della riga successiva corrisponde in realtà a una funzione C (con lo stesso nome) esportata dalla Example_api. 
Per quanto riguarda le strutture C è presente la direttiva @dll.struct ecco come usarla mettendo a confronto la dichiarazione C e la dichiarazione J/Direct.
Dichiarazione di una struttura C:

typedef struct BUFFER_T {
    unsigned int length;
     unsigned char *value;
} BUFFER;


Dichiarazione della medesima struttura con sintassi J/Direct:

/**@dll.struct() */
class BUFFER{
    public int length;
    public int value;
}


Si noti come il puntatore venga dichiarato di tipo int. Altra direttiva è @dll.structmap, usata per specificare informazioni addizionali relative ad una struttura. Eccone un esempio: 
Dichiarazione di una struttura C:

typedef struct DN_T {
    char DName[256];
    char rfc822Name[256];
    char URIName[256];
    char DirName[256];
 } DN;


Dichiarazione della medesima struttura con sintassi J/Direct:
 

/**@dll.struct() */
class DN{
/**@dll.structmap([type=FIXEDARRAY, size=256]) */
public byte[] DName;
/**@dll.structmap([type=FIXEDARRAY, size=256]) */

public byte[] rfc822Name;
/**@dll.structmap([type=FIXEDARRAY, size=256]) */
public byte[] URIName;
/**@dll.structmap([type=FIXEDARRAY, size=256]) */
public byte[] DirName;
}

Mapping di tipi e strutture C

Moltissime funzioni C, si aspettano come parametri di invocazione dei puntatori a variabili da modificare durante l'esecuzione. Ad esempio:

int decrypt(BUFFER *pwd, BUFFER *in_buf,BUFFER *out_buf, int *alghorithm);
presenta un intero, alghorithm, come parametro di ritorno passato per riferimento; la chiamata C è del tipo:
res = decrypt(..., &alghorithm);
In Java non esiste il concetto di puntatore. Per simulare l'effetto di un puntatore e poter utilizzare un parametro come valore di ritorno è necessario passare i parametri per riferimento anziché per valore. In Java la semantica del passaggio dei parametri è definita in base al loro tipo: i tipi primitivi (int, char, ecc.) sono sempre passati per valore mentre gli oggetti sono sempre passati per riferimento. Come passare un tipo primitivo per riferimento?
In Java gli arrays sono oggetti e quindi sono passati per riferimento. Il trucco di J/Direct consiste nell'allocare un array di dimensione 1 avente come unico elemento il tipo primitivo da passare. 
Ad esempio, per invocare decrypt da Java:
 
public class JdExample implements JExampleLowLevelInterface {
...
// Metodo dell’interfaccia JExampleLowLevelIterface
public int decrypt(...) {
    int[] algorithm = new int[1];
    ...
    res=decrypt(...,alghoritm);
    ...
}

// Funzione della libreria Example_api
/** @dll.import("Example_api") */
private static native int decrypt(..,int[] alghoritm);
    ...
}


Java non ha il concetto di struttura (struct) tipico del C. È possibile simulare una struttura con una classe priva di metodi e avente come attributi i campi della struttura. Non è però possibile creare istanze della classe e passarle alle funzioni C come se fossero strutture perché il layout di una classe Java (il modo in cui gli oggetti sono allocati in memoria) viene deciso dall'interprete, non a tempo di compilazione come in C. Inoltre, il garbage collector di Java può spostare a runtime gli oggetti in memoria per ottimizzarne l'uso. Se un oggetto usato per rappresentare una struttura venisse spostato durante l'esecuzione della funzione C gli effetti sarebbero disastrosi perché la funzione si aspetta che la struttura mantenga posizione fissa quando accede ai suoi campi. 
La soluzione di J/Direct è di introdurre una seconda direttiva per indicare che una classe Java viene usata per rappresentare una struttura C e che quindi il garbage collector non ha la facoltà di spostare in memoria le istanze della classe.
La struttura C:

typedef struct {
    unsigned int length;
    DN dn[64]; 
} SET_DN;
diventa in J/Direct
/**@dll.struct() */
class SET_DN {
    public int length;
    /**@dll.structmap([type=FIXEDARRAY, size=65535]) */
    public byte dn[];
}


Una volta definita la struttura attraverso la direttiva @dll.struct come si può accedere ad essa o istanziarla? Di certo non attraverso la semantica ad oggetti (costruttore, chiamate a metodi) ma utilizzando le funzionalità della classe com.ms.dll.DllLib.

int signers_ptr = DllLib.allocCoTaskMem(DllLib.sizeOf(SET_DN));
SET_DN setDn=(SET_DN)DllLib.ptrToStruct(clazz,signers_ptr);
per allocare la struttura SET_DN.
Altro problema: molte strutture C contengono degli arrays tra i loro campi. Il compilatore C ottimizza la memoria allocando lo spazio dell'array all'interno della struttura stessa. In Java invece ogni array viene sempre allocato come oggetto separato. Anche in questo caso l'unico modo per risolvere il problema è di usare una terza direttiva per forzare l'allocazione dell'array all'interno della classe che rappresenta la struttura. Esempio:
Struttura C:
typedef struct {
    char DName[256];
    char rfc822Name[256];
    char URIName[256];
    char DirName[256];
} DN;


Dichiarazione J/Direct:
 

/**@dll.struct() */
class DN{
    /**@dll.structmap([type=FIXEDARRAY, size=256]) */
    public byte[] DName;
    /**@dll.structmap([type=FIXEDARRAY, size=256]) */
    public byte[] rfc822Name;
    /**@dll.structmap([type=FIXEDARRAY, size=256]) */
    public byte[] URIName;
    /**@dll.structmap([type=FIXEDARRAY, size=256]) */
    public byte[] DirName;
}


La direttiva dll.structmap specifica che l'array di dimensione size deve essere allocato all'interno della classe.

Puntatori e Accesso Diretto alla Memoria: classe com.ms.dll.DllLib
Parlare di Java e accesso diretto alla memoria, può sembrare alquanto strano se non assurdo, ma Microsoft è anche questo. J/Direct API contiene in particolare una classe DllLib del package com.ms.dll che offre metodi statici per il collegamento con librerie dinamiche e per l’utilizzo dei puntatori (!). Prima di vedere in dettaglio tali metodi, è logico porsi una domanda: cosa si intende per puntatore in Java?.
Consideriamo ad esempio la funzione C:
 

/** @dll.import(“DLL”) */
BUFFER* DECRYPT(BUFFER* in,...)

la chiamata Java a tale funzione restituisce un puntatore (32 bit) che deve essere dichiarato int:

public class JdDLL implements DLLInterface {
public void f(BUFFER in,...) {
    ...
    int ptr = DECRYPT(in,...);
    BUFFER out;
    out = (BUFFER_P)DllLib.ptrToStruct(clazz,ptr);
    int length = out.length;
    ...
}


Con il metodo ptrToStruct si può trasformare il puntatore nella corrispondente struttura (dichiarata con la direttiva @dll.struct), oppure con i metodi read e write si può avere accesso diretto alla memoria alla quale il puntatore fa riferimento. Un puntatore non è altro che un intero contenente l’indirizzo di un oggetto, ma anche l’oggetto stesso può essere usato con semantica di puntatore. Elenco dei metodi della classe DllLib:

public static int addrOf(int root);
Restituisce l’indirizzo di un oggetto dichiarato con la direttiva @dll.struct. root è l’intero restituito dalla chiamata Root.alloc(Object obj)
public static int allocCoTaskMem(int cb);
Allocazione di un blocco di memoria di dimensione cb, attraverso la funzione Win32 CoTaskMemAlloc.
public static void freeCoTaskMem(int ptr);
Libera il blocco di memoria indicato da ptr attraverso la chiamata Win32 CoTaskMemFree.
public static boolean isStruct(Class structCls);
Indica se una classe è una struttura nativa.
public native static boolean isStruct(Field f);
Indica se un campo appartiene ad una struttura nativa.
public native static int offsetOf(Field f);
public static int offsetOf(Class struct, String field)
Restituisce l’offset (in byte) di un membro di una struttura nativa.
public static String ptrToString(int ptr);
Converte una stringa nativa (TCHAR[]) in una stringa Java
public static String ptrToStringAnsi(int ptr);
Converte una stringa nativa in formato ANSI , in una stringa Java.
public static String ptrToStringUni(int ptr);
Converte una stringa nativa in formato Unicode, in una stringa Java.
public static Object ptrToStruct(Class struct, int ptr);
Restituisce la struttura nativa indicata dal puntatore. Questo metodo si comporta come il cast ad una struttura di un puntatore C.
public static native byte read1(Object ptr, int ofs); 
public static native byte read1(int ptr, int ofs);
public static byte read1(int ptr);
Lettura di un byte nella locazione di memoria indicata (indirizzo base  + offset).
public static native short read2(Object ptr, int ofs);
public static native short read2(int ptr, int ofs);
public static short read2(int ptr);
Lettura di due byte nella locazione di memoria indicata (indirizzo base  + offset).
public static native int read4(Object ptr, int ofs);
public static native int read4(int ptr, int ofs);
public static int read4(int ptr);
Lettura di quattro byte nella locazione di memoria indicata (indirizzo base  + offset).
public static native long read8(Object ptr, int ofs);
public static native long read8(int ptr, int ofs);
public static long read8(int ptr);
Lettura di otto byte nella locazione di memoria indicata (indirizzo base  + offset).
public static native void release(Object obj);
Questo metodo rilascia la memoria associata all’oggetto nativo obj, dichiarato con la direttiva
@dll.struct. Se l’oggetto non è nativo, il metodo non fa nulla.
public native static void resize(Object obj, int size);
Riallocazione di memoria per l’oggetto nativo obj. Questo metodo può essere usato solo se l’oggetto è istanziato con una “new”, e può essere utile per strutture a dimensione dinamica.
public static int sizeOf(Class structCls);
public native static int sizeOf(Object structObj);
Dimensione in byte di una struttura nativa, dichiarata @dll.struct o @com.struct.
public static int stringToCoTaskMem(String s);
Copia di una stringa ANSI o Unicode, in una struttura nativa. Per rilasciare il blocco di memoria si deve usare il metodo freeCoTaskMem.
public static int stringToCoTaskMemUni(String s);
Copia di una stringa in un blocco di memoria nativo, in formato Unicode.
public static final void throwWin32Exception();
Lancia l’eccezione Win32Exception utilizzando l’ultimo codice d’errore settato.
public static native void write1(Object ptr,int ofs, byte val);
public static native void write1(int ptr,int ofs, byte val);
public static void write1(int ptr, byte val);
Scrittura di un byte nella locazione di memoria indicata (indirizzo base + offset).
public static native void write2(Object ptr, int ofs, short val);
public static native void write2(int ptr, int ofs, short val);
public static void write2(int ptr, short val);
public static native void write2(Object ptr, int ofs, char val);
public static native void write2(int ptr, int ofs, char val);
public static void write2(int ptr, char val);
Scrittura di due byte nella locazione di memoria indicata (indirizzo base + offset).
public static native void write4(Object ptr, int ofs, int val);
public static native void write4(int ptr, int ofs, int val);
public static void write4(int ptr, int val);
Scrittura di quattro byte nella locazione di memoria indicata (indirizzo base + offset).
public static native void write8(Object ptr, int ofs, long val);
public static native void write8(int ptr, int ofs, long val);
public static void write8(int ptr, long val);
Scrittura di otto byte nella locazione di memoria indicata (indirizzo base + offset).
 
 
 

Conclusioni
La interoperabilità di Java con altri linguaggi e con “vecchi” componenti software (come le librerie dinamiche) da la possibilità agli sviluppatori di risolvere problemi di compatibilità hardware, di riutilizzo di codice (legacy code) e di efficienza. Essa è possibile attraverso un meccanismo di certificazione delle applet che consente di uscire dal modello di sicurezza Java (Sandbox) e di creare quindi applicazioni Internet nel rispetto dei vincoli di sicurezza, ma allo stesso tempo esenti da limitazioni di accesso alle risorse locali e remote. Ma va osservato che, per non incorrere in banali quanto sgradevoli inconvenienti si deve porre molta attenzione alla progettazione di sistemi di basati su vecchie tecnologie. 
La scarsa separazione dei meccanismi dovuta alla presenza di componenti vecchi e mal progettati, limita l’espressività delle interfacce. L’utilizzo di codice ibrido Java-C limita le potenzialità della tecnologia Java ed in particolare la portabilità e la mobilità del codice, oltre che rivoluzionare completamente le fondamenta sulle quali è basato il linguaggio Java, cioè il modello di sicurezza per il codice mobile e il concetto di garbage collector. Come se non bastasse va osservato che la tecnologia J/Direct introduce il concetto di puntatore alla memoria (peggio non c’è niente!!), totalmente assente in Java. 
Chiaramente la possibilità fornita agli sviluppatori di integrare i vecchi sistemi con Java è fondamentale per lo sviluppo della nuova tecnologia. Dall’esperienza acquisita in questo lavoro posso affermare che Java Native Interface è un modo elegante per integrarsi con librerie dinamiche. Si può accedere alla JVM da codice C (!!), gestire i thread e gli oggetti Java. J/Direct è invece comodo (non si deve programmare in C) e veloce, ma vincolato alla piattaforma Windows. 
Una raccomandazione agli sviluppatori è quella di evitare i metodi nativi e soprattutto, nella progettazione di API, utilizzare modelli che siano in grado di identificare i meccanismi da implementare, tenendo in particolare considerazione il modello Engine-Provider, perché consente
di realizzare sistemi con le proprietà di:

  • Indipendenza dagli algoritmi. Abbiamo una unica interfaccia indipendente dallo specificoalgoritmo usato dal provider.
  • Indipendenza dall'implementazione. L’applicazione non si deve preoccupare dello specifico  provider che implementa il servizio richiesto.
  • Estendibilità degli algoritmi. E’ facile aggiungere nuovi algoritmi e migliorare quelli esistenti, perchè questo non comporta la modifica delle applicazioni che utilizzano i servizi.


Bibliografia

[JAVA100] Sun, 100% Pure Java Program, http://java.sun.com/100percent
[LIVECONN] Netscape,  http://developer.netscape.com/tech/javascript/index.html
[DEV] Netscape, DevEdge Online, http://developer.netscape.com
[MSDN] Microsoft Developer Network, http://msdn.microsoft.com
[JNI] Sun, Java Native Interface Specification,
http://java.sun.com/products/jdk/1.2/docs/guide/jni/spec
[JDIR] Microsoft J/Direct Overview, http://microsoft.com/java/resource/jdirect.htm
[SPI] Sun, Java Cryptography Architecture,
http://java.sun.com/products/jdk/1.2/docs/guide/security/spec
[COM] Microsoft, The Component Object Model Specification,
http://www.microsoft.com/com/comdocs.asp
[JCOM] Microsoft, Integrating Java and COM: http://microsoft.com/java/resource/java_com.htm
 
 
 

Appendice A: Dll, Com e Java a confronto

Tra i più usati componenti software troviamo le librerie dinamiche (Dynamic Link Library). Non esiste programmatore C, C++, Delphi, Visual Basic che non abbia mai creato una DLL. Si tratta di un file contenente codice eseguibile (quindi compilato per una specifica piattaforma), che può essere caricato dinamicamente da altre applicazioni attraverso un meccanismo di link dinamico fornito dal sistema operativo. L’evoluzione di questa tecnologia sviluppatasi prevalentemente in ambiente windows ha portato recentemente alla tecnologia COM (Component Object Model) [COM], nata per risolvere problemi come la complessità delle applicazioni, la riusabilità, l’interoperabilità di applicazioni scritte con linguaggi di programmazione differenti e da produttori diversi, il riuso di software già esistente, il versioning. Si tratta di una architettura basata sul concetti di:

  • Costruzione di “Component objects”.
  • Introduzione di un modello a oggetti.
  • Definizione di un formato binario di rappresentazione standard (binary interoperability standard).
  • Introduzione di un modello per applicazioni distribuite.


La programmazione Object-Oriented consente ai programmatori di costruire potenti e flessibili oggetti, riusabili da altri programmi. Un oggetto è un insieme di operazioni (intelligenza) e di uno stato (dati). Principi fondamentali nella programmazione ad oggetti sono la incapsulazione, e l’information hidding. In altre parole ciò che al cliente interessa il “contratto” che l’oggetto propone, non la specifica implementazione.
La tecnologia COM formalizza la nozione di contratto tra oggetto e cliente in modo da porre le basi per la interoperabilità. Vengono così introdotti gli “Oggetti Componenti”. Questo non basta per la interoperabilità, occorre anche uno standard forte. COM definisce un meccanismo completamente standardizzato per creare “oggetti componenti” e per la comunicazione tra cliente e oggetto. Contrariamente alla tradizionale programmazione Object-Oriented, questo meccanismo è indipendente dall’applicazione che usa gli oggetti e dal linguaggio di programmazione utilizzato per creare gli oggetti. Infatti COM definisce uno standard binario per la interoperabilità. Non si parla di interoperabilità basata sul linguaggio, ma sul formato binario. Nel dominio dei sistemi distribuiti, COM definisce uno standard indipendente dall’architettura, per l’interoperabilità di oggetti su piattaforme eterogenee. COM supporta oggetti distribuiti, cioè consente agli sviluppatori di progettare applicazioni con più oggetti componenti, ognuno dei quali può essere eseguito su una macchina diversa, in modo trasparente. In generale comunque gli oggetti componenti sono incapsulati in una libreria dinamica o in un file eseguibile, c’è quindi una stretta corrispondenza tre file (libreria o eseguibile in formato binario) e componente.
Una estensione orientata a Internet per la tecnologia COM è la più famosa tecnologia ActiveX, che fornisce meccanismi di integrazione con le piattaforme browser e server Web ed in particolar modo consente l’esecuzione di applicazioni Internet attraverso il browser. 
Per quanto riguarda la tecnologia Java va ricordato che questo linguaggio è nato principalmente per sviluppare applicazioni Internet. La rivoluzione Java si basa su:

    Indipendenza dalla piattaforma. Possibile grazie allo standard di compilazione bytecode e alla natura di linguaggio interpretato.
    Mobilità del codice (code on demand). Il Class Loader Java consente di utilizzare codice remoto in modo semplice e sicuro.
    Integrazione con browser e server Web. Oggi praticamente tutti i browser e server Web sono in grado di interpretare codice Java in modo sempre più efficiente.
    Estendibilità e presenza di un ricco ambiente ad oggetti per la progettazione di applicazioni di vario tipo (applicazioni distribuite, grafiche, gestionali).
    Semplicità di programmazione (garbage collector, assenza dei puntatori).


Tutte queste caratteristiche fanno di Java lo standard di fatto per la programmazione di applicazioni distribuite e applicazioni Internet. La differenza fondamentale tra le due tecnologie è che quando si parla di sistemi COM e ActiveX ci si riferisce sempre a codice eseguibile in formato binario e di conseguenza dipendente dal tipo di piattaforma, mentre nel caso di Java, il codice è in formato bytecode e quindi indipendente dalla piattaforma.
 
 
 

Appendice B: Modello SPI (Service Provider Interface)
Il modello che viene preso come esempio è quello relativo all’architettura JCA (Java Cryptography Architecture) [SPI]. Si tratta di una struttura a tre livelli composta da classi engine, classi astratte per la definizione dei servizi (SPI - Service Provider Interface) e provider dei servizi.

Una classe engine definisce un servizio per le applicazioni
Le classi engine, forniscono l'interfaccia per un servizio (indipendente dalla specifica implementazione). Esse definiscono API (Application Programming Interface) che consentono alle applicazioni l'utilizzo dei servizi.
Per ogni classe engine, esiste una classe astratta "SPI" (Service Provider Interface) che definisce quali metodi deve contenere l'implementazione dello specifico servizio. Una istanza della classe engine (oggetto API), incapsula (come campo privato) una istanza della corrispondente classe SPI (oggetto SPI). Tutti i metodi della classe engine sono di tipo final, e non fanno altro che eseguire una chiamata al metodo relativo dell'oggetto SPI incapsulato. Una istanza della classe engine si ottiene attraverso il metodo di tipo factory getInstance(). Ogni classe SPI è astratta. L'implementazione del servizio (Service Provider) deve essere sottoclasse (non astratta) della interfaccia SPI relativa.
Questa astrazione fornisce alla struttura software le proprietà di indipendenza dall’implementazione, interoperabilità delle implementazioni ed estendibilità degli algoritmi. E’ evidente che una struttura simile è estremamente vantaggiosa. Tuttavia, va osservato che essa può essere realizzata solo per quei servizi la cui interfaccia è ben definita e stabile, in caso contrario si verrebbero a perdere i principali vantaggi.
 

Gli esempi allegati all'articolo

Cript_api.txt
gr_cript_jni_JniCript.txt
JCript.txt
JDirectCript.txt
JniCript.c.txt
JniCript.txt
JniCriptNetscape.txt
JniCriptStandard.txt
Chi volesse mettersi in contatto con la redazione può farlo scrivendo a mokainfo@mokabyte.it