Faire discuter C# et C/C++ (et d’autres)

Posted by & filed under Dev'.

Arrrgh !

[ Guide non exhaustif réalisé lorsque j’étais encore stagiaire 😉 ]

Cette exception m’a donné un peu de fil à retordre, je vais donc partager ma solution avec vous.
Dans le cadre d’un projet C#, il arrive qu’on ait besoin d’utiliser des libraires (.DLL) non managées. Pour l’impétrant ce n’est pas forcément une balade de santé…  même en demandant à Google.

La problématique est la suivante : « J’ai trouvé un source C++ génial sur le net qui fait exactement ce que je veux, et j’aimerais l’utiliser dans mon application C#. »

Ceci dit cet article peut vous aider si vous cherchez simplement à utiliser une DLL quelconque en C# (sautez à l’étape deux).

  • Etape 1 (facile) : Je fais quoi de mon source C++ ?

    Vous m’aurez vu venir de loin… pour utiliser notre fameux code C++ nous allons créer une DLL.
    Pour cela, il faut créer un nouveau projet C++ au sein de la solution :

    • Visual C++, Win32 Console Application, puis cocher DLL et Empty project dans l’assistant de création

    Ensuite, on importe les sources dans VC++ (je pars du principe que vous savez utiliser C++ et VC++, le cas échéant, vous devriez trouver moult aide sur le net).

    Enfin on ajoute au projet C++ un .h et .cpp pour l’interface de notre DLL. Dans ce fichier on déclare (.h) et écrit (.cpp) les fonctions que l’on souhaite exposer dans la DLL (aka, utiliser depuis C#).

    Cela peut être simplement des appels vers les fonctions qui vous interessent dans le code C++ ou, et là est l’intérêt, des fonctions qui machent un peu le travail avant de passer la main à C#.
    Pourquoi donc ? Car l’on ne peut exporter une classe de C++ vers .Net, car pour l’instant, le framework ne supporte pas __thiscall. Il va donc falloir se contenter de fonctions échangeant :

    • Des types simples (bool, int, double…)
    • Des pointeurs
    • Des structs

    Pour déclarer une fonction exportée dans la DLL, il suffit de faire précéder sa déclaration (dans le .h donc) de :  __declspec(dllexport)

    Voici en exemple, un bout de mon code, brut de décoffrage. Je cherche à utiliser une classe : voronoicell, je vais donc l’instancier dans ma DLL en tant variable globale statique (car je ne peux pas la manipuler en C#, du moins pas de façon simple), puis les fonctions que j’expose dans la DLL me permettent de jouer avec la-dite instance de classe.

    Fichier dllinterface.h

    #ifndef _VORO_DLL_INTERFACE_
    #define _VORO_DLL_INTERFACE_

    /*
    * Voro++ DLL Interface
    * ==================== * Port of the library Voro++ (only basic functions) as a DLL file, exposing some basics features.
    * Made in order to be integrated in a C# project.

    *

    * Author : Pierre BELIN, 2009.
    */

    namespace VoroDLL
    {
    extern « C »
    {

    __declspec
    (dllexport) void SCAInit(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax);
    __declspec(dllexport) void SCAPlane(double x, double y, double z, double rsq);
    __declspec(dllexport) void SCACleanup(void);
    }
    }

    #endif

    Fichier dllinterface.cpp

    #include « dllinterface.h »
    #include « voro++.cc.h »

    // Note : Compile with /EHsc /LD

    namespace VoroDLL
    {

    static voronoicell * SCA;

    void SCAInit(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax)
    {
    if(SCA != NULL)
    delete SCA;

    SCA = new voronoicell();
    SCA->init((fpoint)xmin, (fpoint)xmax, (fpoint)ymin, (fpoint)ymax, (fpoint)zmin, (fpoint)zmax);
    }

    void SCAPlane(double x, double y, double z, double rsq)
    {
    SCA->plane((fpoint)x, (fpoint)y, (fpoint)z, (fpoint)rsq);
    }

    void SCACleanup(void)
    {
    delete SCA;
    }
    }

    Il ne reste plus qu’a compiler et linker tout ça pour obtenir une jolie petite DLL.

  • Etape 2 : Et pour l’utiliser en C# ?

    Premièrement, pour ne pas se prendre la tête avec les chemins, car Visual Studio, ce gros malin, ne place pas les résultats d’un build C# et d’un build C++ d’une même solution dans les mêmes répertoires par défaut, il est de bon ton de régler le chemin de sortie de votre projet C# à la racine de votre solution.

    • En Debug : VC# sors le projet dans -Solution-/-ProjetC#-/bin/x86/Debug, et VC++ dans -Solution-/Debug.
    • En Release : VC# sors le projet dans -Solution-/-ProjetC#-/bin/x86/Release, et VC++ dans -Solution-/Release.

    Changer les propriétés du projet C# pour une sortie dans ../Debug et ../Release fera l’affaire, et votre .exe C# se trouvera donc dans le même répertoire que votre .dll C++, ce qui épagnera des erreurs « Je trouve pas ta DLL mon coco » à l’exécution.

    Ensuite pour utiliser une DLL non managée au sein de C#, il faut faire appel à DLLImport et au package System.Runtime.InteropServices;
    Personellement, j’ai écrit une classe qui ‘wrappe’ ma DLL, voici ce que ça donne :

    Fichier Voro++.cs :

    using System;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    namespace xxx.Analysis
    {

    /// <summary>
    /// Voro++ DLL Interface
    /// ====================
    /// Port of the library Voro++ (only basic functions) as a DLL file, exposing some basics features.
    /// This library allows to compute Voronoi cells in a 3D space.
    ///
    /// Author : Pierre BELIN, 2009.
    /// </summary>

    class VoroPP
    {
    [DllImport(« Voro++.dll », EntryPoint = « #2 »)]
    private
    static extern void _Init(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax);

    [DllImport(« Voro++.dll », EntryPoint = « #3 »)]

    private
    static extern void _Plane(double x, double y, double z, double rsq);
    [DllImport(« Voro++.dll », EntryPoint = « #1 »)]
    private static extern void _Cleanup();

    public static void Cleanup()
    {

    try
    {
    _Cleanup();

    }
    catch (Exception e)
    {
    MessageBox.Show(« Error interacting with Voro++.dll : «  + e.Message, « DLL Error », MessageBoxButtons.OK, MessageBoxIcon.Error);
    Application.Exit();
    }

    }

    public static void Init(double xmin, double xmax, double ymin, double ymax, double zmin, double zmax)
    {

    try
    {
    _Init(xmin, xmax, ymin, ymax, zmin, zmax);

    }
    catch (Exception e)
    {
    MessageBox.Show(« Error interacting with Voro++.dll : «  + e.Message, « DLL Error », MessageBoxButtons.OK, MessageBoxIcon.Error);
    Application.Exit();
    }

    }

    public static void Plane(double x, double y, double z, double rsq)
    {

    try
    {
    _Plane(x, y, z, rsq);
    }
    catch (Exception e)
    {
    MessageBox.Show(« Error interacting with Voro++.dll : «  + e.Message, « DLL Error », MessageBoxButtons.OK, MessageBoxIcon.Error);
    Application.Exit();
    }

    }

    }

    }

    Vous l’aurez donc compris, pour faire appel à votre fonction exportée, il faut reprendre son prototype en la déclarant static extern et en ajoutant le préfixe DLLImport : [DllImport(« Ma DLL », EntryPoint = « Un numéro obscur ou le nom de la fonction »)]

    L’EntryPoint est sensé marcher avec les noms de fonctions automatiquement…
    Cependant, selon la façon dont à été codée la DLL que vous voulez utiliser, vous pouvez vous manger l’exception en tête d’article…
    La parade dans ce cas est d’appeler les fonctions via leur numéro ordinal, c’est plus moche mais ça fonctionne.

    Pour trouver le numéro ordinal correspondant à votre fonction, il faut ouvrir la DLL avec laquelle vous voulez jouer avec un petit utilitaire, par exemple DLL Export Viewer :

    DLL Export Viewer
    Sinon, comme vous pouvez le voir, je n’utilise pas directement les fonctions de DLL, histoire de pouvoir prendre en compte les exceptions pouvant arriver lors de l’exécution, typiquement DLLNotFound et EntryPointNotFound.
    J’espère vous avoir épargné quelques moments de tâtonnements et frustration 😉

Plus d’infos :

2 Responses to “Faire discuter C# et C/C++ (et d’autres)”

  1. Franz

    Bien sympa et bien expliquer. Je sais pas si ca a voir mais la tes entrypoint sont par ordre alphabetique ^^. Enfin si c’est juste random et que c’est un coincidence c’est marrant.