Intégration d'une DLL C++ avec OpenCV et onnxruntime pour une application C# AnyCPU

Une DLL développée en C++ utilisant OpenCV et onnxruntime doit être appelée depuis une application C# fonctionnant en mode AnyCPU, c'est-à-dire compatible à la fois avec les plateformes x86 et x64.

L'application C# doit placer la DLL C++ et ses dépendances dans un dossier "Libs" séparé plutôt que dans le dossier "Debug".

Difficultés techniques

OpenCV et onnxruntime existent en versions distinctes pour les architectures x86 et x64, qui ne sont pas interchangeables.

Quand la DLL C++ référence un fichier tel que "model.onnx", et que les fichiers sont placés dans "Debug/Libs", le système recherche ce fichier à partir de l'exécutable C# plutôt que depuis l'emplacement de la DLL, provoquant une erreur de fichier introuvable.

Configuration du système

  • Système d'exploitation : Windows 11
  • Environnement de développement : Visual Studio 2019

Solution proposée

  1. Créer deux DLL distinctes en C++ : AnalyseChampignon_x64.dll et AnalyseChampignon_x86.dll
    • Chaque DLL expose des fonctions d'entrée différentes : lancer_x64() et lancer_x86()
    • Utiliser une fonction repertoireDLL() pour récupérer le chemin d'accès de la DLL et construire le chemin absolu du modèle ONNX
  2. En C#, charger dynamiquement la DLL appropriée selon l'architecture système
    • Créer une interface wrapper lancer() qui sélectionne automatiquement la bonne version
    • Utiliser la fonction SetDllDirectory() de "kernel32.dll" pour définir le dossier de dépendances approprié

Implémentation détaillée

Version C++ x64

#include "nlohmann/json.hpp"
#include <fstream>
#include <iostream>
#include <ctime>
#include <sstream>
#include <windows.h>
#include <filesystem>

#include "opencv2/opencv.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/core/utils/logger.hpp"

std::string repertoireDLL()
{
    HMODULE hModule = GetModuleHandleA("AnalyseChampignon_x64.dll");
    char chemin[MAX_PATH];
    memset(chemin, 0, MAX_PATH);
    GetModuleFileNameA(hModule, chemin, MAX_PATH);
    
    std::string cheminComplet = chemin;
    size_t positionSeparateur = cheminComplet.find_last_of('\\');
    return cheminComplet.substr(0, positionSeparateur + 1);
}

extern "C" __declspec(dllexport) const char* lancer_x64(char* cheminImage, char* parametres)
{
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
    std::string dossierDLL = repertoireDLL();
    nlohmann::json resultat;
    
    std::string cheminImageStr = cheminImage;
    traiterImage(cheminImageStr, dossierDLL, parametres, resultat);

    std::string resultatStr = resultat.dump(2);
    char* retour;
    retour = strdup(resultatStr.c_str());
    return retour;
}

Version C++ x86

#include "nlohmann/json.hpp"
#include <fstream>
#include <iostream>
#include <ctime>
#include <sstream>
#include <windows.h>
#include <filesystem>

#include "opencv2/opencv.hpp"
#include "opencv2/imgproc.hpp;
#include "opencv2/highgui.hpp"
#include "opencv2/core/utils/logger.hpp"

std::string repertoireDLL()
{
    HMODULE hModule = GetModuleHandleA("AnalyseChampignon_x86.dll");
    char chemin[MAX_PATH];
    memset(chemin, 0, MAX_PATH);
    GetModuleFileNameA(hModule, chemin, MAX_PATH);
    
    std::string cheminComplet = chemin;
    size_t positionSeparateur = cheminComplet.find_last_of('\\');
    return cheminComplet.substr(0, positionSeparateur + 1);
}

extern "C" __declspec(dllexport) const char* lancer_x86(char* cheminImage, char* parametres)
{
    cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);
    std::string dossierDLL = repertoireDLL();
    nlohmann::json resultat;
    
    std::string cheminImageStr = cheminImage;
    traiterImage(cheminImageStr, dossierDLL, parametres, resultat);

    std::string resultatStr = resultat.dump(2);
    char* retour;
    retour = strdup(resultatStr.c_str());
    return retour;
}

Code C# pour l'appel de la DLL

Structure des dossiers de dépendances :

|-- AnalyseChampignon_x64.dll
|-- AnalyseChampignon_x86.dll
|-- champignon.onnx
|-- modele_2.0.onnx
|-- x64_DependancesAnalyse
| |-- concrt140d.dll
| |-- msvcp140_1d.dll
| |-- msvcp140d.dll
| |-- onnxruntime.dll
| |-- opencv_world455d.dll
| |-- ucrtbased.dll
| |-- vcruntime140_1d.dll
| |-- vcruntime140d.dll
|-- x86_DependancesAnalyse
| |-- concrt140.dll
| |-- concrt140d.dll
| |-- msvcp140.dll
| |-- msvcp140_1d.dll
| |-- msvcp140d.dll
| |-- onnxruntime.dll
| |-- opencv_world455d.dll
| |-- ucrtbase.dll
| |-- ucrtbased.dll
| |-- vcruntime140.dll
| |-- vcruntime140d.dll
using Newtonsoft.Json.Linq;
using System;
using System.Runtime.InteropServices;

namespace IntegrationDLL
{
    class ProgrammePrincipal
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        static extern bool SetDllDirectory(string cheminDossier);

        [DllImport("Libs/AnalyseChampignon_x64.dll", CallingConvention = CallingConvention.Cdecl)]
        extern static IntPtr lancer_x64(string cheminImage);

        [DllImport("Libs/AnalyseChampignon_x86.dll", CallingConvention = CallingConvention.Cdecl)]
        extern static IntPtr lancer_x86(string cheminImage);

        static IntPtr lancer(string cheminImage)
        {
            if (Environment.Is64BitOperatingSystem)
            {
                return lancer_x64(cheminImage);
            }
            else
            {
                return lancer_x86(cheminImage);
            } 
        }
        
        static void configurerDossierDLL()
        {
            string cheminDeps;
            if (Environment.Is64BitOperatingSystem)
            {
                cheminDeps = $"{AppDomain.CurrentDomain.BaseDirectory}Libs\\x64_DependancesAnalyse";
            }
            else
            {
                cheminDeps = $"{AppDomain.CurrentDomain.BaseDirectory}Libs\\x86_DependancesAnalyse";
            }
            
            bool resultat = SetDllDirectory(cheminDeps);
            if (!resultat) 
                throw new System.ComponentModel.Win32Exception();
        }

        static void Main(string[] args)
        {
            try
            {
                string cheminImage = @"D:\\images\\echantillon.jpg";

                // Configuration pour AnyCPU
                configurerDossierDLL();
                IntPtr resultatPtr = lancer(cheminImage);
                string resultatStr = Marshal.PtrToStringAnsi(resultatPtr);

                // Traitement du résultat
                JObject resultatJson = JObject.Parse(resultatStr);
                Console.WriteLine($"Code erreur: {resultatJson["erreur"]}");
                Console.WriteLine($"Détails: {resultatJson["details_erreur"]}");

                // Codes d'erreur:
                // 0: Succès
                // 1: Image non trouvée
                // 2: Modèle dish.onnx manquant
                // 3: Modèle model_2.0.onnx manquant
                // 4: Aucun champignon détecté
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Exception: {ex.Message}");
            }

            Console.ReadLine();
        }
    }
}

Remarque importante

Pour une compilation AnyCPU sur un système x64, il est nécessaire de désélectionner l'option "Préférer 32 bits" dans les paramètres de génération du projet.

Étiquettes: DLL C# OpenCV onnxruntime interop C++/C# AnyCPU

Publié le 1 août à 04h21