mercredi 23 juin 2010

22/06.2010: Les Zozos balayés par les Zoulous

Hop Bof

vendredi 28 mai 2010

Mettre des librairies natives dans un jar, et les utiliser

Certaines applications Java peuvent utiliser du code natif C ou C++ grâce au framework JNI. On parlera pas ici de la manière d'utiliser l'interface JNI, mais de la manière de l'emballer dans l'application finale pour que l'utilisateur n'ait pas à s'en soucier.

En général, une fois le développement terminé, on place les classes dans un fichier jar et on délivre l'application sous cette forme, avec ou sans enrobage supplémentaire tel qu'un installeur.
Avec JNI, outre les classes, on se retrouve avec des librairies natives: une par plateforme supportée.


La question est de savoir que faire de ces librairies. On peut bien sûr les mettre à côté, mais cela oblige à spécifier leur emplacement au moyen de paramètres sur la ligne de commande Java. exemple: java -Dava.library.path=lib_dir .....
Au delà de cette lourdeur, il reste la question de l'identification de la librairie associée à la plateforme. On va voir comment affranchir l'utilisateur de tous ces problèmes.

Dans un premier temps, comme la présence de librairies natives à coté des JAR fait désordre, on va profiter du fait que le jar n'est rien d'autre que tar compressé. On peut y mettre ce que l'on veut, on va donc y mettre nos librairies.

1) Bien nommer les librairies:
On va d'abord renommer nos librairies afin d'identifier clairement l'architecture à laquelle elles se rattachent.
Il existe 2 propriétés java indiquant l'architecture sur laquelle tourne la machine java:
  • os.name: retourne le nom de l'OS (Linux, Win ou Mac)
  • os.arch: retourne l'architecture matérielle (i386, universal...)
On va renommer nos librairies sous la forme suivante: os.name_os.arch.lib, le tout en minuscules afin d'éviter les problèmes de casse. Ainsi, sous Linux 32bits, ma librairie s'appellera linux_i686.lib.

2) Placer les libraires à la racine du fichier JAR.
Cela n'est pas obligatoire, mais ca facilite. Il faut comprendre que le fichier JAR est vu par la JVM comme un répertoire accessible pour rechercher des ressources. On peut donc y organiser nos fichiers à notre guise (sauf pour les .class qui doivent respecter la hiérarchie des paquetages).

exemple: jar cvf mon_application.jar *.lib .....

3) Intégrer dans le code java le chargement des librairies natives.
Avant de faire appel à des méthodes natives, il faut charger le code natif. Le loader de Java sais bien le faire à condition de lui indiquer où chercher.
Cela se fait par étapes:
  1. Construire le nom de la librairie à partir des ressources os.*
  2. Localiser la ressource.
  3. Extraire la bonne librairie
  4. Charger la bonne librairie.
En bon java cela donne (on suppose que la classe native est sqlite.tools.ASCIIDataFileLoader):

public static void loadNativeLibrary() throws Exception {
if( LIB_LOADED == false ) {
String libname = System.getProperty("os.name")
+ "_"
+ System.getProperty("os.arch")
+ ".lib";
ClassLoader cl = Class.forName("sqlite.tools.ASCIIDataFileLoader")
.getClassLoader();
InputStream in = cl.getResourceAsStream(libname);
if (in == null) {
throw new Exception("libname: "
+ libname
+" not found (supposed to be in sqliteimporter.jar)");
}
/*
* Extract the lib file and link it with the app
*/
File tmplib = File.createTempFile("libsqlitejdbc-", ".lib");
tmplib.deleteOnExit();
OutputStream out = new FileOutputStream(tmplib);
byte[] buf = new byte[1024];
for (int len; (len = in.read(buf)) != -1;) {
out.write(buf, 0, len);
}
in.close();
out.close();
System.load(tmplib.getAbsolutePath());

LIB_LOADED = Boolean.TRUE;
}
}


Il faut ensuite lier l'appel de cette méthode à l'appel de chaque méthode native:


class .......
.....
static int importTSV(String table, String file, String db_file)
throws Exception {
ASCIIDataFileLoader.loadNativeLibrary();
return importASCIIFile(table, file, "\t", db_file);
}

native static int importASCIIFile(String table, String file
, String separ, String db_file) throws Exception;
}

Cela ne marche bien que si notre application n'utilise qu'une seule librairie native. Dans le cas contraire, il faut un peu compliquer le nommage afin d'y inclure le nom de la classe ou du paquetage ou de n'importe quoi d'autre évitant toute ambiguïté.

jeudi 27 mai 2010

Faire du JNI sous Eclipse

Rappel
Un petit rappel: JNI (Java Native Interface) est une interface de programmation permettant à des classes JAVA d'inclure du code C ou C++. Il peut paraître paradoxal de vouloir ainsi perdre la portabilité du langage. Il y a toutefois de nombreux cas pour lesquels cette interface est très utile. D'une manière générale, elle permet de récupérer des codes éprouvés, difficile voire impossible à porter en JAVA.

JNI c'est bien mais..
La conséquence de l'utilisation de JNI est qu'il faut (re)mettre les mains dans la programmation C.
Il faut aussi se rapeller que la classe JAVA en question devra être accompagnée des librairies pour toutes les plateformes sur lesquelles elle est susceptible de tourner (MAC, Window, Linux, 32/64bits..). On verra dans un prochain post comment s'y prendre.

Je veux pas quitter Eclipse
Comme beaucoup des utilisateurs d'Eclipse ont du mal à revenir au bon vieux couple emacs/gcc, on va voir comment mettre au point un projet JNI dans Eclipe.

1) Installer CDT sur Eclipse
CDT est le plugin C/C++ d'Eclipse.
- Aller dans Help->Install New Software
- Cliquer sur Available Software Sites
- Cliquer sur add et remplir les champs suivants:
    name = CDT
    URL = http://download.eclipse.org/tools/cdt/releases/galileo

- Selectionner CDT dans le combobox Work With
- Continuer l'installation avec Next/Next/Finish

2) La partie Java
- Créer un projet Java dans Eclipse (JSQLITE dans mon cas).
- Déclarer les méthodes natives là ou bon vous semble. Tant que les classes possédant ce genre de méthodes ne sont pas appelées, tout va bien. Autrement, le linker vous gratifie d'un java.lang.UnsatisfiedLinkError signifiant qu'il n'a pas trouvé le code natif.
- Créer dans le projet un répertoire pour les futurs fichiers headers (jni_headers par exemple). Cela se fait par le menu File->New->Directory.

Il faut maintenant créer les fichiers header contenant les prototypes C des méthodes natives. Pour cela, on peut créer un tache d'exécution en cliquant sur le bouton run surmontant une petite valise rouge.
- Clique droit sur Program sélectionner New et remplir les champs de la manière suivante:
    name: javah
    location: /usr/lib/jvm/jdk1.6.0_16/bin/javah
Il s'agit du chemin d'accès à l'utilitaire Java créant les fichiers headers. Il se trouve dans votre JDK.
    Argument -classpath build -d jni_headers wrapper.SQLiteDataImporter. On suppose que wrapper.SQLiteDataImporter est notre classe avec des méthodes natives

L'exécution de cette tâche doit produire les fichiers d'entêtes dans le répertoire jni_header.

3) La partie C
- Créer un projet C de type Shared Library: menu File->New->C Project, puis, Shared Library->Empty Project. Dans note exemple, ce projet s'appelle JSQLITENative.
- Importer les headers JNI: Clique droit sur le projet, puis Properties->C/C++ General->Paths and Symbols.
- Sélectioner le langage GNU C du tab Includes
- Cliquer sur Add->Workspace, puis sélectionner JSQLITE->jni_headers, valider.
- De la même manière ajouter les répertoire JAVA_HOME/include et JAVA_HOME/include/platform afin d'inclure les headers JNI dans les règles de compilation.
- Vos headers JNI doivent apparaitre dans le dossier Includes
- Créer votre source C: menu File->New->Source File
- Ecriver et compiler votre code. Attention, CDT ne pratique pas la compilation à la volée comme en Java. Il vous faut faire un CTRL B pour compiler. En cas d'erreurs persistentes, un peu de nettoyage peut arranger les choses: menu Project->Clean....
- La librairie doit être placée dans le répertoire Binaries.

4) Faire tourner le code Java.
Il faut d'abord indiquer ou se trouve la librairie C.
- Ouvrir la configuration de lancement de votre application Java: Bouton Run puis Run Configuration.
- Sélectionner votre application.
- Ouvrir le tab Argument
- Mettre la ligne suivant dans la zone VM Arguments: -Djava.library.path="${workspace_loc:SQLITENative/Debug}".
Attention le répertoire Debug est le nom du répertoire de votre Workspace ou se trouve la librairie C. Il ne s'agit pas de son avatar figurant dans l'explorateur de projets qui est Binaries.
- N'oublier pas charger la librairie dans le main de votre application: System.loadLibrary("SQLITENative"); pour un libraire dont le nom est libSQLITENative.so.


Voilà, c'est sommaire, mais ca peut aider. N'oublier pas de refaire cette manip sur chacune des plateforme pour lesquelles vous voulez implémenter vos classes JNI.

jeudi 18 février 2010

Mode curseur et transactions avec JDBC (PostgresQL)

Lorsque l'on travaille en mode curseur sous JDBC, le mode auto-commit du connecteur est nécessairement dévalidé:

connection.setAutoCommit(false);

La conséquence est que le postmaster garde en mémoire le log des requêtes.
Si l'on fait plusieurs requêtes avec le même connecteur JDBC, on reste connecté au même Postmaster et ce dernier explose en mémoire.

J'ai essayé d'insérer des commits entre les requêtes, mais cela a produit l'erreur suivante:

DECLARE CURSOR pourrait seulement être utilisé dans des blocs de transaction

J'avoue en pas très bien comprendre le sens de ce message et je me suis résolu à créer un nouveau connecteur à chaque requête en mode curseur:


public static ResultSet runLargeQuerySQL(String sql) throws FatalException {
try {
closeLargeQueryConnection();
jdbc_large_connection = DriverManager.getConnection(Database.getConnector().getJdbc_url(),Database.getConnector().getJdbc_reader(), Database.getConnector().getJdbc_reader_password());
Statement _stmts =jdbc_large_connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
jdbc_large_connection.setAutoCommit(false);
_stmts.setFetchSize(1000);
if( Messenger.debug_mode ) Messenger.printMsg(Messenger.DEBUG, "Select large query: " + sql);
/*
* Trailing semicolumns make the statement to run in non-cursor mode
*/
return _stmts.executeQuery(sql.replaceAll(";", ""));
} catch (Exception e) {
Messenger.printMsg(Messenger.ERROR, "Query: " + sql);
Messenger.printStackTrace(e);
FatalException.throwNewException(SaadaException.DB_ERROR, e);
}
return null;
}

/**
* @throws SQLException
*/
public static void closeLargeQueryConnection() throws SQLException {
if( jdbc_large_connection != null ) {
if( Messenger.debug_mode ) Messenger.printMsg(Messenger.DEBUG, "Close connection for large queries ");
jdbc_large_connection.close();
jdbc_large_connection = null;
}

}

mercredi 17 février 2010

Quand le mode curseur de JDBC ne fonctionne pas sur PostgresQL

Imaginer la requete suivante sur une grosse table (10.000.000 lignes):

SELECT oidprimary,oidsecondary FROM Counterparts

Comme vous ne voulez pas que votre application Java vous fasse un OutOfMemoryError, vous traitez votre requête en mode curseur (voir ici)):

connection().setAutoCommit(false);
Statement _stmts = connection.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
_stmts.setFetchSize(1000);
ResultSet rs = _stmts.executeQuery("SELECT oidprimary,oidsecondary FROM Counterparts");
....

Et ca marche, mais attention, si à la suite d'un couper/coller malicieux par exemple, votre requête se voit suivie d'un ";", vous risquez de vous retrouver avec cette erreur:

Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space

En effet, le pilote JDBC de PostgresQL considère alors que votre requête comporte une suite de requêtes séparées par des ";" et dans ce cas il quitte le mode curseur et tente de recopier tout les résultat en mémoire.

jeudi 19 février 2009

PostgresQL vs MySQL

Mon application a été construite sur PostgresQL, système de base de données auquel on accède par JDBC. Dès le départ il a été envisagé d'utiliser d'autres SGDBs. Nous avons toutefois attendu une demande formelle avant de passer à l'acte.
C'est ce qui s'est passé récemment avec un utilisateur confessant que notre Saada serait encore plus génial s'il pouvait utiliser MySQL en lieu et place de Postgres et que autrement il ne lui serait d'aucun intérêt.
Vous trouverez plus loin un bestiaire des incompatibilités SQL entre les deux systèmes. Il ne s'agit pas ici de comparer mais simplement de pointer les différences.

Les transactions


L'ouverture de transaction permet de rendre atomique l'exécution d'un ensemble de requêtes. Si l'une plante, la base retombe dans son état initial (rollback) quelque soit les modifications déjà effectuées avant le plantage.






PostgresMySQL
Ouvrir la transaction
BEGIN TRANSACTIONSTART TRANSACTION
Terminer la transaction
COMMITCOMMIT
Annuler la transaction
ABORTROLLBACK

Une différence plus importante concerne le verrouillage des tables. Sous Postgres, le verrouillages des tables durant une transaction est implicite. Il n'y a pas à s'en occuper.
Sous MySQL, il faut verrouiller explicitement toutes les tables auxquelles on accède durant la transaction.

  • Si au cours de la transaction une requête modifie la table TABLE_W il faut la faire précéder par LOCK TABLE TABLE_W WRITE

  • Si au cours de la transaction une requête lit la table TABLE_R il faut la faire précéder par LOCK TABLE TABLE_R READ

  • Si au cours de la transaction une requête lit les tables TABLE_R1 et TABLE_R2 et qu'elle modifie la table TABLE_W, il faut la faire précéder par LOCK TABLE TABLE_R1, TABLE_R2 READ, TABLE_W WRITE


Les tables temporaires


Un petit bonnet d'âne à MySQL: La partie Web des bases Saada utilise un rôle (compte) d'accès à la base avec des droits réduits (le reader) de manière a éviter à des requêtes malicieuses d'altérer les données. Seulement voila, certaines requêtes compliquées utilisent des tables temporaires. Cela nous permet d'éviter des jointure compliquées dont on ne sait jamais comment l'optimiseur du moteur de requête se sortira.
Sous Postgres, il n'y a rien à faire de particulier, le reader peut créer ses tables temporaires.
Sous MySQL, la musique est bien différente. Il faut donner explicitement au reader le droit de créer des tables temporaires:GRANT CREATE TEMPORARY TABLES ON database TO reader

Le chargement de données à partir de fichiers


Lors du chargement de donnée dans MySQL à partir d'un fichiers ASCII (LOAD DATA INFILE...) les contraintes de clés primaires sont vérifiées ligne par ligne, et ça rame vraiment beaucoup. La commande SQL suivante permer de dévalider temporairement les contraintes sur la clé primaire.
ALTER TABLE tbl_name DISABLE KEYS

vendredi 14 novembre 2008

Tout a changé le 4 Novembre 2008

Il est des convictions tellement fortes qu'elles ne peuvent s'ancrer que dans la matière malléable et fertile d'un esprit d'enfant. Mais alors elles peuvent rester des décades durant rangées dans notre cerveau au rayon des évidences, des axiomes devrais-je dire:
Depuis ma naissance et jusqu'à un certain 4 Novembre 2008, j'étais totalement certain que jamais je ne serai plus vieux que le président des USA.