Per comprendere l’utilizzo di una tecnologia non c’è nulla di meglio che provarla. Quale migliore esempio se non il classico “Hello, World!”?
Il codice che analizzeremo tra breve è preso direttamente dagli esempi presenti nella guida ufficiale.
HelloWorld.java
package it.spicydev.blog.jna;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;
/**
* Date: 1/21/12 7:06 PM
*
* @author Mircha Emanuel `ryuujin` D'Angelo
* @version 1.0
*/
public class HelloWorld {
/**
* Java interface to hold the native library methods extending the Library interface
*/
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary) Native.loadLibrary(
(Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class);
void printf(String format, Object... args);
}
public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
CLibrary.INSTANCE.printf("%d %f\n",1,3.14);
}
}
Eseguendolo avrete (se tutto andrà bene) il seguente output:
Hello, World
1 3.140000000
La prima cosa da fare è identificare quale libreria nativa vogliamo utilizzare. Nel nostro primo esempio vogliamo utilizzare la C Standard Library. In ambiente GNU/Linux la libreria è “c” (libc). Per sistemi Ms Windows® si tratta di “msvcrt” (msvcrt.dll) a contenere la libreria C standard.
Il nostro esempio consente di scegliere quale referenziare a seconda della piattaforma in cui sta girando.
com.sun.jna.Platform contiene dei metodi per accedere facilmente a informazioni sulla piattaforma senza dover ricorrere alle System Properties (es. System.getProperty(“os.name”)).
Nella classe definiamo un’interfaccia che fungerà da Singleton per instanziare una sola volta la libreria. Non è l’unico metodo a nostra disposizione, nei successivi articoli ne vedremo altri.
/**
* Java interface to hold the native library methods extending the Library interface
*/
public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary) Native.loadLibrary(
(Platform.isWindows() ? "msvcrt" : "c"), CLibrary.class);
void printf(String format, Object... args);
}
Il metodo per instanziare una libreria è Native.loadLibrary(String name, Class interfaceClass). name è il nome della libreria dinamica che deve essere disponibile nel percorso predefinito e definito dal sistema operativo o nel percorso indicato dalla system property jna.library.path.
Nell’interfaccia definiamo anche i metodi Java che rispecchiano i metodi da esporre dalla libreria nativa. Questi avranno la stessa interfaccia dei metodi nativi.
Esiste una tabella di corrispondenza tra i tipi nativi e i tipi in Java e all’occorrenza la libreria ha delle classi appropriate per rappresentare strutture, puntatori, …
Un estratto di questa corrispondenza è la seguente tabella:
Native Type | Size | Java Type | Common Windows Types |
char | 8-bit integer | byte | BYTE, TCHAR |
short | 16-bit integer | short | WORD |
wchar_t | 16/32-bit character | char | TCHAR |
int | 32-bit integer | int | DWORD |
int | boolean value | boolean | BOOL |
long | 32/64-bit integer | NativeLong | LONG |
long long | 64-bit integer | long | __int64 |
float | 32-bit FP | float | |
double | 64-bit FP | double | |
char* | C string | String | LPTCSTR |
void* | pointer | Pointer | LPVOID, HANDLE, LPXXX |
Spesso è necessario avere a che fare con tipi più complessi. Il passaggio di strutture, ad esempio, sia per valore sia per riferimento.
Per il prossimo esempio faremo uso di una libreria scritta da noi: JNATestLib. La libreria è scritta in C ed è molto semplice. Definisce una struct che rappresenta i dati utente USER_INFO e due metodi:
- compileUserInfo
- accetta una stringa, un intero e un puntatore a USER_INFO
- printUserInfo
- accetta USER_INFO (passato per valore)
Di seguito il sorgente, l’header e le istruzioni per compilare la libreria in Linux:
Header C JNATestLib.h
#ifndef JNA_TEST_LIB
#define JNA_TEST_LIB
typedef struct tagUserInfo {
char name[256];
int age;
} USER_INFO;
#ifdef __cplusplus
extern "C" {
#endif
int compileUserInfo(const char* name, int age, USER_INFO *user_info);
void printUserInfo(USER_INFO user_info);
#ifdef __cplusplus
}
#endif
#endif
JNATestLib.c
#include
#include
#include "JNATestLib.h"
int compileUserInfo(const char* name, int age, USER_INFO *user_info){
if(strlen(name)>256){
return -1;
}
strcpy(user_info->name,name);
user_info->age = age;
return 0;
}
void printUserInfo(USER_INFO user_info){
printf("USER INFO:\n");
printf("name: %s\n",user_info.name);
printf("age: %d\n",user_info.age);
}
Compilazione:
gcc -c -fPIC JNATestLib.c -o JNATestLib.o
gcc -shared -Wl,-soname,libJNATestLib.so.1 -o libJNATestLib.so.1.0.1 JNATestLib.o
Se utilizzate Ubuntu è sufficiente installare il pacchetto build-essential (sudo apt-get install build-essential).
A questo punto vi ritroverete con un file libJNATestLib.so.1.0.1. Per installare la libreria nel sistema è sufficiente creare un link nella directory /usr/lib e dare il comando sudo ldconfig -v.
Un po’ di teoria prima dell’esempio. Per rappresentare in JNA una struct C estendiamo la classe astratta com.sun.jna.Structure provveduta dalla libreria. Come indica il Javadoc, Structure consente di rappresentare una struct e di default, quando è usata come valore di ritorno o paramentro di funzione, viene passata per riferimento. Imprimetevelo bene in mente: come comportamento standard Structure viene passato per riferimento.
Il nostro esempio è molto utile perché richiede sia il passaggio per riferimento sia il passaggio per valore.
Prima di tutto proviamo solo ad implementare la chiamata al primo metodo: compileUserInfo.
La nostra struttura può essere implementata in questo modo:
/**
* USER_INFO Structure
*/
protected class USER_INFO extends Structure{
public final byte[] name;
public final int age;
public USER_INFO() {
name = new byte[256];
age = 0;
allocateMemory();
}
@Override
public String toString(){
return new String(name);
}
}
Abbiamo mappato i due cambi della struct in C:
- name: byte[]
- age: int
É indispensabile definire un costruttore dove effettuare l’inizializzazione dei campi. Se ci sono array o altri campi che richiedono allocazione di memoria é necessario chiamare il metodo allocateMemory() (é consigliabile invocarlo sempre).
Essendo una classe Java a tutti gli effetti possiamo aggiungere anche altri metodi, nel nostro esempio abbiamo aggiunto un override al metodo toString().
Quindi per implementare la chiamata al solo metodo compileUserInfo della libreria nativa JNATestLib è sufficiente il sorgente che segue:
JNAPart2es1.java
package it.spicydev.blog.jna;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Structure;
/**
* Date: 1/23/12 10:56 PM
*
* @author Mircha Emanuel `ryuujin` D'Angelo
* @version 1.0
*/
public class JNAPart2es1 {
/**
* USER_INFO Structure
*/
protected class USER_INFO extends Structure{
public final byte[] name;
public final int age;
public USER_INFO() {
name = new byte[256];
age = 0;
allocateMemory();
}
@Override
public String toString(){
return new String(name);
}
}
/**
* Singleton instance of JNATestLib (JNATestLib.so.1)
*/
private interface JNATestLib extends Library {
JNATestLib INSTANCE = (JNATestLib) Native.loadLibrary("JNATestLib", JNATestLib.class);
int compileUserInfo(String name, int age, USER_INFO user_info);
}
private USER_INFO user_info;
public void testLibrary() {
user_info = new USER_INFO();
JNATestLib.INSTANCE.compileUserInfo("Mircha Emanuel D'Angelo", 29, user_info);
System.out.println(user_info);
}
public static void main(String[] args) {
JNAPart2es1 jnaTestLib = new JNAPart2es1();
jnaTestLib.testLibrary();
}
}
L’output del programma mostra come user_info è stato correttamente compilato dalla chiamata al metodo nativo:
Mircha Emanuel D'Angelo
Se adesso provassimo a implementare la chiamata al secondo metodo nativo, senza modificare il nostro codice, incorreremo ad un errore. Provate a capire perché? Come viene passata di default al metodo nativo la Structure? Il metodo nativo printUserInfo richiede che USER_INFO venga passato per valore, ma di default Structure viene passato per riferimento.
Per far ciò modifichiamo il comportamento standard della Structure facendogli implementare l’interfaccia di “annotazione” Structure.ByValue.
class USER_INFO extends Structure implements Structure.ByValue {
Adesso é necessario modificare l’interfaccia del metodo compileUserInfo indicandogli che il terzo paramentro é un puntatore (com.sun.jna.Pointer).
La classe Pointer é l’astrazione di un puntatore nativo e ne rappresenta la corrispondenza in Java. Può essere il riferimento di memoria di qualsiasi oggetto.
La classe Structure implementa un metodo Pointer getPointer() che ritorna un “puntatore” alla struttura. Il Javadoc del metodo richiede che se il “puntatore” é usato come argomento è necessario invocare esplicitamente write() prima e read() dopo getPointer().
Ecco il sorgente completo per il nostro esercizio.
JNAPart2es2.java
package it.spicydev.blog.jna;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
/**
* Date: 1/23/12 10:56 PM
*
* @author Mircha Emanuel `ryuujin` D'Angelo
* @version 1.0
*/
public class JNAPart2es2 {
/**
* USER_INFO Structure (ByValue)
*/
protected class USER_INFO extends Structure implements Structure.ByValue{
public final byte[] name;
public final int age;
public USER_INFO() {
name = new byte[256];
age = 0;
allocateMemory();
}
}
/**
* Singleton instance of JNATestLib (JNATestLib.so.1)
*/
private interface JNATestLib extends Library {
JNATestLib INSTANCE = (JNATestLib) Native.loadLibrary("JNATestLib", JNATestLib.class);
int compileUserInfo(String name, int age, Pointer p_user_info);
void printUserInfo(USER_INFO user_info);
}
public void testLibrary() {
USER_INFO user_info = new USER_INFO();
user_info.write();
JNATestLib.INSTANCE.compileUserInfo("Mircha Emanuel D'Angelo", 29, user_info.getPointer());
user_info.read();
JNATestLib.INSTANCE.printUserInfo(user_info); //errore: Structure is byReference
}
public static void main(String[] args) {
JNAPart2es2 jnaTestLib2es = new JNAPart2es2();
jnaTestLib2es.testLibrary();
}
}
Questa volta l’output del programma sarà stampato direttamente dal metodo nativo printUserInfo:
USER INFO:
name: Mircha Emanuel D'Angelo
age: 29
Ciao Mircha,
ho davvero apprezzato la preziosa guida all’uso di jna con java, per muovere i primi passi.
Ora però, mi sono imbattuto in una dll particolare. La dll ha solo una funzione che riceve 2 array e ne restituisce un terzo con i risultaati.
Lo sviluppatore, nella documentazione scrive:
Warning: positions 14,15 and 30 of the output array contain BSTR values, so in order to convert them to CString (in
Visual C++) is possible to create an instance of the class CString passing aRis[n].bstrVal to the constructor (see
VARIANT structure declaration).
Inoltre, lo sviluppatore ha reso disponibile un esempio di classe c++:
#include “calcdlld.h”
void CCalcdllsvrView::OnCalcolo()
{
// TODO: Add your control notification handler code here
VARIANT aRis[NRESDATA];
double aInp[NINPUTDATA];
double aOpt[NOPTIONSDATA];
// Collect data from input mask
GetData(vInp);
// Check for errors
if (!StartJob(aInp, aRis, aOpt))
return;
// Show results
ShowResults(aRis);
}
Con Java, ho provato in moltissimi modi….ma non ci sono proprio riuscito ad ottenere un risultato sulla terza matrice aRis.
Questa è la mia ultima soluzione dove almeno sono riuscito a far funzionare la classe senza però ottenere un risultato interpretabile:
public class dbm {
public static void main(String[] args) {
dbmDll library = dbmDll.INSTANCE;
double[] aInp = new double[100];
double[] aOpt = new double[1];
aInp[0]=1;
aInp[1]=32;
aInp[2]=50;
aInp[5]=12550;
aInp[15]=16;
aInp[16]=2.5d;
aInp[18]=1500;
aInp[22]=18;
aInp[26]=7;
aInp[27]=12;
aInp[32]=35;
aInp[40]=1;
aInp[45]=1;
aInp[50]=11;
aInp[61]=1;
//aInp[62]=0.11d;
//aInp[63]=0.4d;
Pointer pointer = new Memory(100);
byte[] idByteArray = pointer.getByteArray(0, 100);
library.StartJob(aInp,idByteArray,aOpt);
}
public interface dbmDll extends Library {
dbmDll INSTANCE = (dbmDll) Native.loadLibrary(“calcdll”, dbmDll.class);
void StartJob(double[] aInp, byte[] idByteArray, double[] aOpt);
}
}
Aiuto!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!