C++ Dll-in C# verwenden



  • Hallo zusammen,

    ich hab eine C++-DLL-Datei erstellt und nun stellt sich die Anforderung, diese auch unter C# nutzen zu können. Die DLL besitzt Win32-Schnittstellen, sollte dahingehend also eigentlich kompatibel sein.
    Mir stellt sich nun die Frage, wie die Interface-Anbindung unter C# aussehen muss. Geht das überhaupt?

    Hier das Beispiel einer der exportierten DLL-Funktionen:

    __declspec(dllexport) bool MeineFunktion(const int &Wert1, char* Wert2);
    // Wert1 ist ein übergebener Wert, Wert2 soll ein Rückgabewert sein
    

    Soweit ich bisher gelesen habe, müsste man da wohl am besten eine eigene Klasse mit "managed definitions" in C# dafür anlegen. Wie müsste das für die obige Funktion dann ausschauen?

    Ausserdem enthält die DLL die Möglichkeit, von dort heraus eine Callback-Funktion aufrufen zu lassen. Diese Funktion muss aus C# heraus an die DLL übergeben werden. Geht das überhaupt?

    Ich arbeite mit dem VS 2005.
    Für hilfreiche Tipps schon mal im Voraus vielen Dank!
    Wes



  • hab zwar noch net wirklich was mit unmanaged dlls in c# gemacht aber generell müsste es

    [DllImport("Any.dll")]
    private static extern bool MeineFunktion(int Wert1, char Wert2);
    

    sein



  • Fast richtig.

    [DllImport("Any.dll")]
    public static extern bool MeineFunktion(int Wert1, StringBuilder Wert2);
    

    Am besten schaust du dir die Interop Geschichte mal genauer an.
    Ich bin mir nicht sicher, ob man ein delegate als Funtionszeiger übergeben kann, was aber auf jeden Fall geht ist

    [DllImport("Any.dll")]
    public static extern void setCallback(IntPtr functionPointer);
    public delegate void MeinDelegate(parameter);
    ...
    
    MeinDelegate deleg = new MeinDelegate(function);
    ...
    
    IntPtr fnPtr = Marshal.GetFunctionPointerForDelegeate(deleg);
    setCallback(fnPtr);
    

    Wie gesagt, bin mit nicht sicher ob du dir den Umweg über marshaling sparen kannst und direkt das delegate übergeben kannst. Hab den Code auch nicht getested, kann also nich 100%ig garantieren dass es so funktioniert 🙄, aber ich würde mir die Interop auf jeden Fall genauer anschauen.



  • Danke - das werde ich mal so ausprobieren.

    Hm, eine Frage noch... müsste es korrekt nicht so heißen:

    [DllImport("Any.dll")]
    public static extern bool MeineFunktion(int Wert1, out StringBuilder Wert2);
    

    ...oder impliziert die Angabe von StringBuilder schon, dass es ein Rückgabewert ist?



  • Nein, wenn eine Zeichenkette als Parameter ein Rückgabeparameter ist, muss, glaube ich, eine StringBuilder verwendet werden (ohne ref oder out).



  • Asloooo....

    Ok, die Funktionsdefinitionen hat C# so genommen. Dazu habe ich eine eigene Klasse angelegt und darin alle Schnittstellen entsprechend definiert.
    Beim Compilieren gab es keine Fehler. Allerdings dann bei der Ausführung, denn dort findet er die angegebenen Funktionen nicht in der DLL.
    Die Frage ist nun, warum nicht?

    Da fällt mir ein: Ich hatte noch vergessen zu erwähnen, dass die Funktionsrümpfe innerhalb eines Namespaces in der DLL stehen. Müsste ich dann in C# einen Namespace gleichen Namens anlegen? Oder muss die Angabe der zu importierenden Funktion dann nur anders lauten? Wenn ja, wie müsste diese dann aussehen?

    Hier nochmal die (jetzt vollständige) Art, wie es von der DLL exportiert wird:

    namespace MeinNamespace
    {
      __declspec(dllexport) bool MeineFunktion(const int &Wert1, char* Wert2);
      // Wert1 ist ein übergebener Wert, Wert2 soll ein Rückgabewert sein
    }
    

    Und so hab ich es jetzt in C# stehen:

    using System.Runtime.InteropServices;
    
    class MeineDLL
    {
      [DllImport("MeineDLL.dll")]
      public static extern bool MeineFunktion(int Wert1, out string Wert2);
    }
    

    Anwendung:

    namespace TestAppl
    {
      class Program
      {
        static void Main(string[] args)
        {
          string eintext;
          if (MeineDll.MeineFunktion(5, eintext))
            Console.WriteLine("Geht. Ergebnis: '{0}'", eintext);
          else
            Console.WriteLine("Geht nicht.");
        }
      }
    }
    

    Besten Dank schon mal im Voraus für weitere Tipps,
    Wes



  • Ergänzung:
    ----------------

    Nun hab ich mal testweise den Namespace aus der DLL entfernt.
    Trotzdem klappt der Aufruf nicht. Folgende Meldung erscheint:

    Eine nicht behandelte Ausname des Typs
    "System.EntryPointNotFoundException" ist in TestAppl.exe 
    aufgetreten.
    
    Zusätzliche Informationen: Der Einstiegspunkt MeineFunktion wurde
    nicht in der DLL MeineDLL.dll gefunden.
    

    Also scheint es erst einmal nicht am Namespace zu liegen.
    Allerdings macht mich das nun noch ein bisschen ratloser... 😞



  • Du wirst große Schwierigkeiten haben, das Name Mangling von C++ mit den Erwartungen von C# unter einen Hut zu bringen. Unter anderem der angesprochene Namespace, aber auch die Signatur (Funktionsparameter) spielen dabei eine Rolle bei der Erzeugung des Namens. Die Funktion heißt nämlich in der DLL nicht einfach "MeineFunktion", sondern so etwas wie "_MeinNamespace__MeineFunktion_i4cp4". Das findet C# nicht.

    Eine Möglichkeit ist, für die Funktion C-Linkage zu erzwingen, dann kannst Du sie sogar im Namespace belassen.

    namespace MeinNamespace
    {
      extern "C" __declspec(dllexport) bool MeineFunktion(const int &Wert1, char* Wert2);
    }
    


  • Ja, das hat geholfen. Danke!

    Der Aufruf klappt nun. Allerdings bekomme ich beim Aufruf eine AccessViolationException. Aber da muss ich erst mal schauen, wo die herkommt...

    Ggf. melde ich mich nochmal, falls ich am Boden zerstört bin.

    Danke nochmal 🙂



  • Kommt von deinem String. "out string" oder "ref string" geht soviel ich weiß nicht, vermutlich weil es ein "immutable type" ist. Stringbuilder ohne out oder ref verwenden und am besten vorher noch mit der Maximallänge der Zeichenkette initialisieren



  • Nein, von da kommt das nicht. Das mit dem StringBuilder funktioniert so auch nicht. Wie soll dass System denn wissen, dass es sich um einen Rückgabewert handelt, wenn man es nicht angibt? Da muss auf jeden Fall ein out rein, dann kommt auch Text zurück. Anderenfalls nicht.
    Aber für die AccessViolation ist das nicht verantwortlich. Die kommt aus der DLL. Ist aber ein anderes Thema - darum auch nix mehr für hier 😉



  • Mal was anderes: Der erste Parameter wird doch als Referenz auf int übernommen. Könnte mir vorstellen dass das nicht mit dem in C# als Wert übergebenen Argument zusammenpasst. Vermutlich wird die Funktion den Wert als Adresse interpretieren und beim Zugriff auf den eigentlichen Wert knallt's dann.



  • Neenee, das funktioniert schon alles. Mein Problem liegt woanders. In der DLL werden beim 1. Aufruf eine Reihe von globalen Objekten instanziert und dorther kommt auch die Exception. Seltsamerweise aber nur, wenn die DLL von C# aus aufgerufen wird. Eine C++ Applikation hat damit keine Probleme...

    Der Funktionsaufruf an sich funktioniert so wie angegeben einwandfrei und ohne Fehler. Hab ich mit einer kleinen Testanwendung schon durchgeprüft. Trotzdem Danke für den Tipp 🙂



  • Im Übrigen - auch das mit der Callback-Funktion konnte ich nun lösen.
    Wie das geht, hab ich hier gefunden:

    http://msdn2.microsoft.com/de-de/library/843s5s5x(VS.80).aspx





  • Hmm...
    Wenn ich das recht verstehe, müsste man mit StringBuilder doch vorher schon wissen, wie lang der String werden könnte, oder? Wenn das so ist, empfinde ich das für einen Parameter-Rückgabewert doch aber eher als Nachteil. Die C++-DLL kann sich nicht um die Speicherorganisation des StringBuilder-Objekts kümmern. Und die Größe des Textes kann beliebig sein...

    In meinem Fall handelt es sich in der DLL tatsächlich um einen Null-terminierten Text, der auch nach Aufruf der externen Funktion noch im Speicher weiterexistiert. Aus diesem Grund wird auch nur ein Zeiger auf den Textanfang zurückgegeben. Meines Erachtens dürfte dafür ein string-Objekt doch ausreichend sein. Oder verstehe ich da etwas falsch?



  • Ja:

    Der wichtigste Unterschied zwischen den beiden Klassen besteht darin, dass Zeichenfolgen unveränderlich sind, während der Inhalt von StringBuilder-Puffern durch den Aufgerufenen geändert und zurück in den Aufrufer kopiert werden kann.

    Du kannst natürlich auch einen IntPtr per ref oder out übergeben, und dann den Speicher parsen bis du auf die Nullterminierung stösst. Wenn du aber ungefähr sagen kannst, wie lang deine Zeichenkette maximal ist, würde ich einen Stringbuilder empfehlen.



  • Ok, es geht weiter...

    Ein Problem besteht bei Aufruf der Callback-Funktion, wenn Parameter mit übergeben worden sind. Eine Callback-Funktion ohne Parameter wird anstandslos aufgerufen, kehrt zurück zur DLL ohne Fehlermeldung.
    Sobald ich jedoch die Callbackfunktion mit einem Parameter versehe (der Einfachheit halber ein int-Parameter), wird bei der Rückkehr zur DLL folgende Fehlermeldung erzeugt:

    The value of ESP was not properly saved across a function call.  
    This is usually a result of calling a function declared with one calling 
    convention with a function pointer declared with a different calling convention.
    

    Wenn ich das richtig sehe, besagt die Meldung, dass der Stack nach Rückkehr aus der Callback-Funktion nicht mehr stimmt. Genauer gesagt, dass die Größe des übergebenen Parameters in der Deklaration der Callbackfunktion (C++) auf der einen Seite, und die Größe, die sie letztendlich in der dann definierten Callbackfunktion auf der anderen Seite (C#) nicht übereinstimmen.

    Wie kann das denn sein? Sind die Aufrufmechanismen für Methoden bei beiden Systemen denn unterschiedlich?

    Damit es nachvollziehbar wird, gebe ich hier einmal den kompletten Quelltext der DLL und des aufrufenden C#-Programms an.

    DLL-Header

    #pragma once
    
    #ifdef DLLCSTEST_EXPORTS
    #define DLLCSTEST_API extern "C" __declspec(dllexport)
    #else
    #define DLLCSTEST_API __declspec(dllimport)
    #endif
    
    //	Callback-Funktionstyp deklarieren
    typedef void tCallback (int Wert1);
    typedef tCallback* tpCallback;
    
    namespace MyNamespace
    {
      // exportierte Funktionen
      DLLCSTEST_API int fnDllCsTest(int a);
      DLLCSTEST_API void SetCallback(tpCallback Callback);
    }
    

    DLL-Body

    #include "stdafx.h"
    #include "DllCsTest.h"
    
    tpCallback g_pCallback = NULL;
    
    BOOL APIENTRY DllMain( HANDLE hModule, 
                           DWORD  ul_reason_for_call, 
                           LPVOID lpReserved)
    {
      return TRUE;
    }
    
    // Callback-Funktionen setzen
    void MyNamespace::SetCallback(tpCallback Callback)
    {
      g_pCallback = Callback;
    }
    
    // Testfunktion, um Callback aufzurufen
    int MyNamespace::fnDllCsTest(int a)
    {
      if (g_pCallback)
      {
        g_pCallback(a);  // Aufruf der Callback-Funktion
      }
      return a;
    }
    

    C# - Hauptprogramm

    using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
    
    namespace WindowsApplication1
    {
      static class Program
      {
        /// <summary>
        /// Der Haupteinstiegspunkt für die Anwendung.
        /// </summary>
        [STAThread]
        static void Main()
        {
          Application.EnableVisualStyles();
          Application.SetCompatibleTextRenderingDefault(false);
    
          // Callback-Funktionszeiger erzeugen und übergeben
          Callback myCallback = new Callback(Program.DllCallback);
          DllCsTest.SetCallback(myCallback);
    
          int a;
          // Callback-Aufruf auslösen
          a = DllCsTest.fnDllCsTest(1);
    
          Application.Run(new Form1());
        }
    
        public static void DllCallback(int Wert1)
        {
          return;  // der Inhalt von Wert1 stimmt an dieser Stelle
        }
      }
    }
    

    C# - DLL-Anbindung

    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    
    // Callback-Funktionstyp definieren
    public delegate void Callback(int Wert1);
    
    class DllCsTest
    {
      [DllImport("DllCsTest.dll")]
      public static extern int fnDllCsTest(int a);
      [DllImport("DllCsTest.dll")]
      public static extern void SetCallback(Callback pFn);
    }
    

    Testweise kann man ja mal den Wert1 der Callback-Funktion weglassen, damit man sieht, dass der Aufruf an sich korrekt funktioniert.
    Der Wert, der an die Callback-Funktion übergeben wird, hat übrigens innerhalb der Callback-Funktion den korrekten Inhalt.

    Tja, kann mir da jemand einen Tipp geben, woran das Problem liegen könnte?
    Besten Dank schon mal im Voraus,
    Wes



  • Ergänzung:

    Hier noch etwas für die Insider, die auch ein wenig Assembler können...
    Ich hab mir mal den Assembler-Code angeschaut und die Register vor und nach dem Aufruf. Und da habe ich folgendes festgestellt:

    - die zu übergebenden Parameter werden vor Aufruf der Callback-Funktion ordnungsgemäß auf den Stack gelegt.
    - die Callback-Funktion unter C# fragt die Parameter ordnungsgemäß vom Stack ab
    - Und jetzt kommts! Die Parameter werden noch VOR dem Rücksprung ins aufrufende Programm vom Stack entfernt! Schlimm deshalb, weil das normalerweise doch erst NACH dem Rücksprung ins aufrufende Programm erledigt werden soll (und auch wird)!
    - nach Rückkehr, werden die Parameter nochmals vom Stack genommen, was natürlich nun zu einem völlig falschem Stackpointer führt!

    Wie kann denn sowas sein? Gibt es dafür irgendwelche Schalter oder Optionen, mit denen man das korrigieren kann? Eventuell gibt es ja noch bestimmte Aufrufkonventionen, die dem C++ Programm sagen, dass es sich bei der aufgerufenen Funktion um eine aus C# handelt und darum anders zu handhaben ist...

    Ich bin da echt ein bisschen ratlos...



  • Gut, nun zum Abschluss und damit nachfolgende Sucher auch eine Lösung bekommen:

    Wie das mit den Aufrufen und der korrekten Behandlung des Stacks geht, dazu hab ich im Thread Aufruf-Konventionen ändern - wie geht das? was geschrieben.

    Ein weiterer Punkt war ja noch, dass die Parameter nicht korrekt übergeben worden sind. Vor allem, wenn es sich um Pointer (IntPtr) oder strings oder Rückgabewerte handelt, gab es massiv Probleme. Die Lösung liegt darin, dass ALLE Parameter, die an eine C++-DLL-Funktion übergeben werden, als ref deklariert sein müssen und nicht als out! Anderenfalls werden z.B. die Inhalte der Pointer und nicht die Pointer selbst als int-Wert übergeben, was natürlich falsch ist.

    Gruß,
    Wes


Anmelden zum Antworten