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
|