Utiliser une librairie C/C++ non managée dans un code c# (suite)

Comme nous avons pu le voir dans un article précédent, le wrapping de code C++ en C# peut être très simple, malheureusement les choses peuvent rapidement se compliquer !

En effet, continuons de travailler avec la dll GNU Scientific Library, et essayons d’utiliser une gls_matrix, la déclaration dans la documentation est la suivante :

typedef struct
{
    size_t size1;
    size_t size2;
    size_t tda;
    double * data;
    gsl_block * block;
    int owner;
} gsl_matrix;

La documentation nous explique aussi qu’un gls_block est la structure décrite ci dessous et que l’allocation d’une gsl matrix se fait via la fonction suivante :

typedef struct
{
    size_t size;
    double * data;
} gsl_block;

Function: gsl_matrix * gsl_matrix_alloc (size_t n1, size_t n2)

La première chose à faire est de déclarer une structure de type gsl_block en C#

[StructLayout(LayoutKind.Sequential)]
internal class gsl_block
{
    public uint size = 0;
    public IntPtr data = IntPtr.Zero;
}

On commence par déclarer un type struct et indiquer comment le ‘layout’ doit être effectué puis on déclare la class avec ses attributs. On peut constater la présence d’un type IntPrt , c’est la manière C# de gérer les pointeurs! (vue simplifiée mais bien pratique ^^). On peut maintenant déclarer le type gls_matrix comme suit !

    [StructLayout(LayoutKind.Sequential)]
    internal class gsl_vector
    {
        public uint size = 0;
        public uint stride = 0;
        public IntPtr data = IntPtr.Zero;
        public gsl_block block;
        public int owner = 0;
    }

Il ne reste maintenant plus qu’à compléter la fonction d’allocation gls_matrix_alloc. Là malheureusement il faut vraiment se plonger dans le code de la GSL pour trouver les sources et les adapter. En faisant ça on constate que l’on aura aussi besoin de la fonction gls_block_alloc. Le code des deux fonctions nécessaires sont si dessous :

        internal static gsl_block gsl_block_alloc(uint n)
        {
            gsl_block b = new gsl_block();
            b.data = Marshal.AllocCoTaskMem((int)n * sizeof(double));
            b.size = n;
            return b;
        }

        internal static gsl_matrix gsl_matrix_alloc(uint n1, uint n2)
        {
            gsl_matrix b = new gsl_matrix();
            b.size1 = n1;
            b.size2 = n2;
            b.tda = n2;
            b.block = gsl_block_alloc(n1 * n2);
            b.data = b.block.data;
            b.owner = 1;
            return b;
        }

Le point sensible l’implémentation est de bien comprendre ou est allouée la mémoire, en fait toutes les données sont stockées dans le gls_block c’est donc là qu’il faut alloquer la mémoire, le reste n’est qu’un jeu de pointeur. Bien sur une fois la matrice obtenu vous n’êtes pas encore au bout de vos peines, il faut maintenant coder les fonctions capables de libérer la mémoire allouée :

        internal static void gsl_block_free(gsl_block b)
        {
            if (b != null && b.data != null)
            {
                Marshal.FreeCoTaskMem(b.data);
                b.data = IntPtr.Zero;
            }
            b.size = 0;
        }
        internal static void gsl_matrix_free(gsl_matrix m)
        {
            if (m == null)
                return;
            gsl_block_free(m.block);
            m.block = null;
            m.data = IntPtr.Zero;
            m.size1 = 0;
            m.size2 = 0;
            m.tda = 0;
            m.owner = 0;
        }

Là encore, il faut libérer la mémoire marshallée dans la gsl_block, pour le reste on pourrait se contente de tout remettre à zéro. Dernière étape, allouer et récupérer des valeurs aux éléments de la matrice :

        [DllImport("libgsl.dll")]
        internal static extern void gsl_matrix_set(gsl_matrix m, uint i, uint j, double x);
        [DllImport("libgsl.dll")]
        internal static extern double gsl_matrix_get(gsl_matrix m, uint i, uint j);

Vous pouvez constater que cette fois je ne définis pas d’entry point dans la dll car le nom C# et le nom C++ sont identiques! Bien sur une gsl_matrix toute seule ne sert pas à grand chose, mais en ajoutant les fonctions de la GSL permettant de faire une décomposition de chloesky ou tout autre opération matricielle …

Utiliser une librairie C/C++ non managée dans un code c#

Quiconque à déjà programmé en C, C++ et C# connaît les qualités et défauts de chacun de ses langages, le C rapide mais très bas niveau, le C# s’exécutant via une plateforme (.net) donc plus lent, mais par contre extrêmement pratique lorsque les sorties de ce que l’on peut appeler calculateur doivent être interfacée avec une Interface graphique, qu’il s’agisse de winform ou de sites ASP et le C++ qui permet une programmation objet tout en restant assez bas niveau (ami puristes ne vous offusquez pas il s’agit d’une description sommaire, mon but n’est pas de lister qualités défauts et implémentations de chacun de ses langages !).

Seulement voilà, des fois il est nécessaire d’utiliser du cote C++ dans une application codée en C#… si il se trouve que votre librairie est non managée là c’est le drame ! Enfin le drame, pas tout à fait: C# implémente des fonctions qui permettent d’adapter , ‘wrapper’ pour utiliser le mot en usage les fonctions de la dll et de les réutiliser dans votre application!

Par exemple j’ai du réutiliser la librairie GSL dans un projet devant être réalisé en C#. Problème numéro 1 : les sources telles quelles ne peuvent pas être utilisées dans un environnement Windows, heureusement de fiers codeurs en ont développé une version dll . Seulement voilà, une fois que j’ai la dll, que faut-il faire pour réutiliser les fonctions ?

Plusieurs solutions existent, en particulier utiliser du code « unsafe » autorisant les pointeurs en C#, mais je préfère ‘Marshaller » les types de données, c’est à dire indiquer comment doivent être gérés les passages de paramètres par rapport à leur type. Par exemple essayons d’utiliser la fonctions gsl_stat_mean décrite dans la documentation GSL comme : double

gsl_stats_mean (const double data[], size_t stride, size_t n)

This function returns the arithmetic mean of data, a dataset of length n with stride stride.

Il est dit : la fonction retourne un double, prend un paramètre un tableau de double et deux paramètre de type size_t, quelques recherches dans la documentation  et sur google nous apprennent que les paramètre size_t peuvent être assimilés à des int, on connaît donc toutes les informations sur la fonction, il ne reste plus qu’à indiquer ces données au code C#

[DllImport("libgsl.dll", EntryPoint = "gsl_stats_mean")]
internal static extern double mean(
[In] [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] double[] data,
[In] int stride,
[In] int n
);

Décomposons un peu cette fonction.

  1. [DllImport(« libgsl.dll », EntryPoint = « gsl_stats_mean »)] : on utilise une dll qui s’appelle « libgsl.dll » et le point d’entrée, c’est à dire la fonction de la dll que l’on appelle est : « gsl_stats_mean »
  2. internal static extern double mean( : internal , cette fonction ne peut être appelée que dans ce projet, static pas de soucis, extern : le code de la fonction n’est pas présent mais vient d’une source externe. double c’est le type qui est retourné par la fonction et mean correspond au nom C# de la fonction.
  3. [In] [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] double[] data, : premier paramètre de la fonction mean. [In] signifie que le paramètre doit être Marshaller du C# a la dll C++ mais qu’il ne sera pas modifié par la fonction C++ (sinon ou utilise [In, Out] voir [Out] tout seul! MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3) : le type du paramètre est un tableau (LPArray) et c’est le troisième paramètre de la fonction qui en indique la taille (ceci est facultatif). double[] data : type c# du paramètre et nom du paramètre dans la fonction C++
  4. Idem pour les paramètres      [In] int stride et   [In] int n sauf que le type int n’a pas à être marshallé.

Pour appeler la fonction rien de plus simple :

Main(){
double [] test = {1,2,3};
Console.WriteLine(ClassGsl.mean(test,1,3));
Console.Read();
}

et on obtient : 2!

Il n’y a plus qu’à reproduire ce schéma pour toutes les fonctions que l’on souhaite appeler, évidement quand des structures s’en mêlent tout devient un peu plus compliqué… un prochain article sur le sujet dans très peu de temps !