C++ library integration into PostGIS (in French)

Intégration d’une librairie C++ dans PostgreSQL -Première Partie

J’ai trouvé une petite librairie en C++ qui permet d’aligner deux rasters et je voudrais que cette fonctionnalité soit implémentée dans PostGIS en SQL.

Position du problème

Lorsqu’on lit la doc officielle, voici ce que l’on trouve concernant l’usage de librairies C++ dans Postgres:

  • Toutes les fonctions accessibles par le serveur doivent présenter une interface en C ; seules ces fonctions C pourront alors appeler du code C++. Ainsi, l’édition de liens extern C est nécessaire pour les fonctions appelées par le serveur. Ceci est également obligatoire pour toutes les fonctions passées comme pointeur entre le serveur et du code C++.

  • Libérez la mémoire en utilisant la méthode de désallocation appropriée. Par exemple, la majeure partie de la mémoire allouée par le serveur l’est par appel de la fonction palloc(), aussi, il convient de libérer ces zones mémoire en utilisant la fonction pfree(). L’utilisation de la fonction C++ delete échouerait pour ces blocs de mémoire.

  • Évitez la propagation d’exceptions dans le code C (utilisez un bloc catch-all au niveau le plus haut de toute fonction extern C. Ceci est nécessaire, même si le code C++ n’émet explicitement aucune exception, dans la mesure où la survenue d’événements tels qu’un manque de mémoire peut toujours lancer une exception. Toutes les exceptions devront être gérées et les erreurs correspondantes transmises via l’interface du code C. Si possible, compilez le code C++ avec l’option -fno-exceptions afin d’éliminer entièrement la venue d’exceptions ; dans ce cas, vous devrez effectuer vous-même les vérifications correspondantes dans votre code C++, par exemple, vérifier les éventuels paramètres NULL retournés par la fonction new().

  • Si vous appelez des fonctions du serveur depuis du code C++, assurez vous que la pile d’appels ne contienne que des structures C (POD). Ceci est nécessaire dans la mesure où les erreurs au niveau du serveur génèrent un saut via l’instruction longjmp() qui ne peut dépiler proprement une pile d’appels C++ comportant des objets non-POD.

  • Pour résumer, le code C++ doit donc être placé derrière un rempart de fonctions extern C qui fourniront l’interface avec le serveur, et devra éviter toute fuite de mécanismes propres au C++ (exceptions, allocation/libération de mémoire et objets non-POD dans la pile).

Intégration dans PostgreSQL d’une classe C++

Nous allons à travers cet article, détailler toutes les étapes permettant d’intégrer une classe dans PostgreSQL. Le programme de travail est simple:

  1. C++: Création d’une classe simple, et mise en oeuvre via un main simple aussi.
  2. C++: On wrap tout ça avec du “extern C”
  3. C: mise en oeuvre via un main en C
  4. Intégration dans PostgreSQL et création de la fonction SQL associée

Dans une seconde partie, nous mettrons ce plan de bataille en oeuvre pour créer une fonction SQL qui prendra deux rasters en entrée et les alignera grâce à une librairie dont je présenterai l’usage ultérieurement: ST_AlignRaster(rast raster1, rast raster2).

Pour commencer construisons une classe élémentaire

Les codes suivants sont adaptés de eikke et teddy

Commençons par une classe “MyClass” quasi vide, un entier qui se définit et se lit:

Le fichier MyClass.h, qui se passe de commentaires


#ifndef __MYCLASS_H
#define __MYCLASS_H

class MyClass {
        private:
                int m_i;
        public:
                void int_set(int i);
                int int_get();
};

#endif

De même que le fichier MyClass.cc:


#include "MyClass.h"
void MyClass::int_set(int i) {
        m_i = i;
}

int MyClass::int_get() {
        return m_i;
}

MyMain_c++.cc:


#include "MyClass.h"
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
        MyClass *c = new MyClass();
        c->int_set(3);
        cout << c->int_get() << endl;
        delete c;
}

On compile, on lie, on execute:

g++ -c MyClass.cc -o MyClass.o
g++ -c MyMain_c++.cc -o MyMain_c++.o
g++ MyMain_c++.o MyClass.o -o MyMain_c++
./MyMain_c++

On a bien 3 en sortie.

On enveloppe le tout à base de “extern “C”

Maintenant on écrit les wrapper permettant l’usage de la classe en C:

MyWrapper.h, que l’on incluera dans le code C:


#ifndef __MYWRAPPER_H
#define __MYWRAPPER_H

typedef void Interface;

#ifdef __cplusplus
extern "C" {
#endif

	Interface *newMyClass();

	void MyClass_int_set(Interface *v, int i);

	int MyClass_int_get(Interface *v);

	void deleteMyClass(Interface *v);

#ifdef __cplusplus
}
#endif
#endif

MyWrapper.cc:

#include "MyClass.h"
#include "MyWrapper.h"

extern "C" {
        Interface *newMyClass() {
			MyClass* x=new MyClass();
			return (Interface *)x;
			}

        void MyClass_int_set(Interface *v, int i) {
		
			/* Attention à l'erreur naïve suivante: caster v ne crée pas l'objet!!
			(MyClass*)v->int_set(i);
			Il faut déclarer et créer un objet intermédiaire, x par exemple:
			Ne peut-on pas éviter toutes ces copies??*/
	  
		  MyClass* x=(MyClass*)v;
		  x->int_set(i);
	  
		  }

        int MyClass_int_get(Interface *v) {
			MyClass* x=(MyClass*)v;
			return x->int_get();
			}

        void deleteMyClass(Interface *v) {
			MyClass* x=(MyClass*)v;
			delete x;
			}
}

On compile ce wrapper, c’est encore du C++:

g++ -c MyWrapper.cc -o MyWrapper.o

On exploite cette classe depuis du C

Enfin le main(), en C: MyMain_C.c

#include "MyWrapper.h"
#include <stdio.h>

int main(int argc, char* argv[]) {
        Interface *c = newMyClass();
        MyClass_int_set(c, 3);
        printf("%i\n", MyClass_int_get(c));
        deleteMyClass(c);
}

On compile le main, on le lie au wrapper et à la classe, enfin on crée l’executable:

$gcc -c MyMain_c.c -o MyMain_c.o
$g++ MyWrapper.o MyMain_c.o MyClass.o -o MyMain_c

qui sort 3 après un

$./MyMain_c

Variante: Compilation du Wrapper en Librairie partagée

Reprenons les phases de compilation et d’édition des liens en créant une librairie partagée libMyWrapper.so. Pourquoi ce challenge? Car PostgreSQL n’acceptera d’intégrer du code externe que s’il se trouve dans une telle librairie. Il me semble donc pertinent, de regarder dans un cas simple ce qui change relativement à une compilation/édition de liens directe.

Si le code ne change pas, en revanche les modalités de compilation ne sont plus les mêmes.

Commençons par créer le fichier objet de notre classe élémentaire “MyClass”.

L’option -fPIC crée un code dont la position est indépendante, ce qui est nécessaire pour les librairies partagées:

$g++ -c -fPIC MyClass.cc -o MyClass.o

De même, compilons MyWrapper. Dans un premier temps, nous créons le fichier objet:

$g++ -c -fPIC MyWrapper.cc -o MyWrapper.o

Dans un second temps nous créons la librairie partagée, à partir de ce fichier objet:

$g++ -shared -o libMyWrapper.so MyWrapper.o MyClass.o

Nous sommes alors en mesure de tester cette librairie. En premier lieu, nous compilons MyMain.c:

$gcc -c MyMain_c.c -o MyMain_c.o

Et, pour obtenir l’exécutable, nous le lions à notre librairie:

$gcc MyMain_c.o -lMyWrapper -L. -o Main_lib

Noter le -L. qui indique au linker de chercher dans le répertoire courant (”.") les dépendances requises. Si la compilation fonctionne bien, l’exécutable en revanche, non:

$ ./Main_lib 
./Main_lib: error while loading shared libraries: libMyWrapper.so: cannot open shared object file: No such file or directory

Et oui! Au moment de la compilation, l’usage du -L a permis au linker de trouver la librairie, mais aucune mention d’emplacement n’est écrite dans le code, à l’exécution la librairie n’est plus trouvée. Nous avons alors deux solutions (au moins):

  • Modifier la variable d’environnement LD_LIBRARY_PATH:
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/C++/Extern_for_C_library

Et alors:

$ ./Main_lib 
3
  • Ou, reprendre la dernière édition des liens, en prenant le soin d’indiquer dans le code l’emplacement de la librairie partagée. Cela est possible grâce à l’option -rpath:
$unset LD_LIBRARY_PATH
$gcc MyMain_c.o -lMyWrapper -L. -Wl,-rpath=. -o Main_lib
$ ./Main_lib 
3

Notons que dans cette approche, nous n’avons plus besoin du compilateur C++ dès que la librairie est créée.