Austausch von Daten zwischen einer VB.NET-exe und einer C-DLL, 32 und 64 Bit

    • VB.NET

    Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von RodFromGermany.

      Austausch von Daten zwischen einer VB.NET-exe und einer C-DLL, 32 und 64 Bit

      Immer wieder taucht die Frage auf, wie Prozeduren und Parameter deklariert sein müssen, um mit einer C-DLL kommunizieren zu können.
      Eines sofort: Die Kommunikation mit einer C++-DLL ist so aus .NET nicht möglich, da ein unmanaged C++-Objekt erstellt werden muss, und das ist aus .NET nicht möglich, dazu bedarf es eines Wrappers in Form einer C-DLL.
      Deswegen beschränken wir uns hier ausschließlich auf C-DLLs.
      Primär ist stets, wie eine Prozedur in der C-DLL deklariert wurde, danach richtet sich die Übertragung in .NET.
      Zur Information:
      In C wird unterschieden, ob die Parameter einer Prozedur von vorn nach hinten oder von hinten nach vorn auf dem Stack abgelegt werden. Dies wird in der Deklaration der Funktion durch PASCAL bzw. _stdcall unterschieden.
      Außerdem muss jede exportierte Prozedur als solche gekennzeichnet werden.
      Ich habe folgende Beispiele implementiert:
      • ein einfacher Aufruf: TestFunction(), es wird eine MessageBox ausgegeben.
      • Schreiben eines Integer-Wertes in die DLL: SetValue(int value)
      • Auslesen dieses Integer-Wertes aus der DLL: GetValue(int* value)
      • Schreiben von n Integer-Werten in die DLL: SetValues(int* values, int anzahl)
      • Auslesen dieser n Integer-Werte aus der DLL: GetValues(int* values, int anzahl)
      • Schreiben eines ANSI-Strings in die DLL: SetStringAnsi(char* text)
      • Auslesen dieses ANSI-Strings aus der DLL: GetStringAnsi(char** text)
      • Schreiben eines Unicode-Strings in die DLL: SetStringUnicode(wchar_t* text)
      • Auslesen dieses Unicode-Strings aus der DLL: GetStringUnicode(wchar_t** text)
      Die C-DLL-Funktionen sind im File DLL32.cpp implementiert. Trotz seiner Extension CPP handelt es sich um einen C-File, dies erspart einige Projektänderungen.
      Die Übergabeparameter werden abgetestet. Bei einem falschen Aufruf (zu große Anzahl von Werten im Array) kommt eine 0 zurück, bei Erfolg einen 1 (False bzw. True).
      Ihr fügt Eurem Projekt eine MFC-DLL hinzu und fügt das File DLL32.cpp diesem DLL-Projekt hinzu. Fertig.
      Das VB.NET-Projekt besteht aus der Klasse Dll, darin sind die Deklarationen der DLL-Funktionen enthalten
      sowie der Klasse Form1 mit beispielhaften Aufrufen dieser DLL-Funktionen.
      Der mit SetValue() übergebene Wert wird aus einer TextBox ausgelesen, so kann die Funktion überprüft werden.
      Ebenso werden die übergebenen ANSI- bzw. Unicode-Strings aus Textboxen ausgelesen.
      Die rückgelesenen Werte werden in Labels ausgegeben.
      ----
      Wichtig ist die richtige Verpackung der Parameter für die Übergabe an die DLL.
      .NET überwacht den Speicher der angelegten Variablen, C / C++ tut dies nicht. Deswegen muss .NET den Inhalt der Variablen in einen nicht überwachten Speicher kopieren, der dann an die DLL übergeben werden kann. Dieser wird von der DLL ausgelesen bzw. befüllt, danach wird er in .NET ggf. ausgelesen und stets wieder freigegeben.
      Die Daten sind dann in Variablen vom Typ IntPtr abgelegt, das rein- und rauskopieren wird in der Klasse Marshal durchgeführt.
      ----
      Dieser Beitrag stellt keine vollständige Auflistung möglicher Parameterübergaben zwischen .NET und einer C-DLL dar, sondern er soll Anhaltspunkte geben, wie mit .NET-Parametern verfahren werden muss, um sie in eine C-DLL übergeben zu bekommen.

      Vllt. noch ein paar Anmerkungen:
      Der Ausführungspfad ist noch der von meinem Rechner, da müsst Ihr noch den Euren einfügen.
      Im PostBuild wird die DLL und die PDB in das .NET-Debug-Verzeichnis kopiert, da müsst Ihr natürlich dann Eure Pfade eintragen.
      Die Dateien PartMFC.cpp und PartMFC.h sind ein leere C++-Klasse, da kann über das Objekt theApp nach C++ verzweigt werden.
      ----
      Bei den String-Transfer-Prozeduren im VB-Code habe ich vergessen, den nicht gemanageden Speicher aufzuräumen, da muss 4x folgende Zeile als jeweils letzte Zeile der Prozedur rein:

      VB.NET-Quellcode

      1. Marshal.FreeHGlobal(p1) ' vergessen
      Dateien
      • Test_DLL_32.zip

        (39,83 kB, 843 mal heruntergeladen, zuletzt: )
      • DLL32.zip

        (776,22 kB, 663 mal heruntergeladen, zuletzt: )
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „RodFromGermany“ () aus folgendem Grund: Anmerkungen editiert

      Im zweiten Schritt wollen wir über eine Struktur Daten mit einer DLL austauschen.
      Zuerst die
      Struktur in C / C++

      C-Quellcode

      1. typedef enum MyTestEnum
      2. {
      3. a0,
      4. a1,
      5. a2,
      6. a3,
      7. } MyTestEnum;
      8. typedef struct TestStruct
      9. {
      10. int index;
      11. BOOL flag;
      12. MyTestEnum testValue;
      13. char text[256];
      14. } TestStruct;

      In .NET ist es elementar nicht möglich, in einer Struktur ein Array einzufügen, da .NET Arrays stets mit New instanziieren will.
      Um ein solches Array zwischen .NET und C zu übertragen, müssen wir 2 Dinge beachten:
      1. muss das Array als Array mit einer bestimmten Größe gemMarshalt werden,
      2. muss das Array instanziiert werden.
      Zu diesem Zweck benötigt die Struktur einen Konstruktor.

      VB.NET-Quellcode

      1. Public Sub New()
      2. End Sub
      geht leider nicht:

      Nehmen wir also einen Konstruktor mit einem Boolen Dummy-Parameter:

      VB.NET-Quellcode

      1. Public Sub New(xx As Boolean)
      2. End Sub
      Das .NET-Äquivalent zur obigen Struktur sieht nun so aus:
      Struktur in VB.NET

      VB.NET-Quellcode

      1. Imports System.Runtime.InteropServices
      2. ' ...
      3. Public Enum MyTestEnum
      4. a0
      5. a1
      6. a2
      7. a3
      8. End Enum
      9. <StructLayout(LayoutKind.Sequential)> _
      10. Public Structure TestStruct
      11. Public index As Integer
      12. ' C-BOOL nach V.NET-Boolean
      13. <MarshalAs(UnmanagedType.Bool, SizeConst:=4)> _
      14. Public flag As Boolean
      15. Public testValue As MyTestEnum
      16. <MarshalAs(UnmanagedType.ByValArray, SizeConst:=256)> _
      17. Public text As Char()
      18. Public Sub New(xx As Boolean)
      19. index = 0
      20. flag = False
      21. testValue = MyTestEnum.a0
      22. text = New Char(255) {}
      23. End Sub
      24. End Structure
      und in C#

      C-Quellcode

      1. using System.Runtime.InteropServices;
      2. // ...
      3. public enum MyTestEnum
      4. {
      5. a0,
      6. a1,
      7. a2,
      8. a3
      9. }
      10. [StructLayout(LayoutKind.Sequential)]
      11. public struct TestStruct
      12. {
      13. public int index;
      14. [MarshalAs(UnmanagedType.Bool, SizeConst = 4)] // C-BOOL nach C#-bool
      15. public bool flag;
      16. public MyTestEnum testValue;
      17. [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
      18. public char[] text;
      19. public TestStruct(bool xx)
      20. {
      21. index = 0;
      22. flag = false;
      23. testValue = MyTestEnum.a0;
      24. text = new char[256];
      25. }
      26. }
      Gleichzeitig lassen wir uns vom Marshal ein C-BOOL (4 Byte) in ein .NET-Boolean (1 Byte) übersetzen,
      mit solch Übersetzungs-Anweisungen lassen sich "Ungereimtheiten" fein säuberlich vermeiden, insbesondere dass das Strukturelement als 1-Byte pro Char (ANSI) behandelt wird.
      ----------------------
      So. Nun wollen wir die Struktur über die Sprachbarriere heben.
      Wie schon im obigen Post wird die Struktur in einen IntPtr verpackt und Herr Marshal stellt die Routinen bereit, die Struktur-Information in den IntPtr hinein- und wieder herauszuholen:

      VB.NET-Quellcode

      1. Marshal.StructureToPtr(...)
      2. ' bzw.
      3. Marshal.PtrToStructure(...)

      Die Übergabe-Routinen sehen nun so aus:
      Aufruf von VB.NET

      VB.NET-Quellcode

      1. Private Sub btnStruct_Click(sender As System.Object, e As System.EventArgs) Handles btnStruct.Click
      2. Dim success As Boolean = False
      3. Dim st As New Dll.TestStruct(True)
      4. ' Werte in die DLL
      5. st.index = 1
      6. st.flag = False
      7. st.testValue = Dll.MyTestEnum.a2
      8. Dim txt = "bla bla bla"
      9. 'st.text = "bla".ToCharArray ' funktioniert nicht, Instanz nicht in der Struktur
      10. For i = 0 To Math.Min(st.text.Length, txt.Length) - 1
      11. st.text(i) = txt(i)
      12. Next
      13. ' Speicher bereitstellen
      14. Dim p1 As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(st))
      15. ' Daten hineinschreiben
      16. Marshal.StructureToPtr(st, p1, True)
      17. ' an die DLL übergeben
      18. success = Dll.SetStruct(p1)
      19. ' Daten zurückholen
      20. Dim st2 As Dll.TestStruct = CType(Marshal.PtrToStructure(p1, GetType(Dll.TestStruct)), Dll.TestStruct)
      21. ' Speicher wieder freigeben
      22. Marshal.FreeHGlobal(p1)
      23. ' DLL-Text anzeigen
      24. MessageBox.Show(New String(st2.text), "Value from DLL")
      25. End Sub
      Aufruf von C#

      C-Quellcode

      1. private void button1_Click(object sender, EventArgs e)
      2. {
      3. bool success = false;
      4. Dll.TestStruct st = new Dll.TestStruct(true);
      5. // Werte in die DLL
      6. st.index = 1;
      7. st.flag = false;
      8. st.testValue = Dll.MyTestEnum.a2;
      9. dynamic txt = "bla bla bla";
      10. //st.text = "bla".ToCharArray(); // funktioniert nicht, Instanz nicht in der Struktur
      11. for (int i = 0; i < Math.Min(st.text.Length, txt.Length); i++)
      12. {
      13. st.text[i] = txt[i];
      14. }
      15. // Speicher bereitstellen
      16. IntPtr p1 = Marshal.AllocHGlobal(Marshal.SizeOf(st));
      17. // Daten hineinschreiben
      18. Marshal.StructureToPtr(st, p1, true);
      19. // an die DLL übergeben
      20. success = Dll.SetStruct(p1);
      21. // Daten zurückholen
      22. Dll.TestStruct st2 = (Dll.TestStruct)Marshal.PtrToStructure(p1, typeof(Dll.TestStruct));
      23. // Speicher wieder freigeben
      24. Marshal.FreeHGlobal(p1);
      25. // DLL-Text anzeigen
      26. MessageBox.Show(new string(st2.text), "Value from DLL");
      27. }
      Zum Schluss noch die in der DLL aufgerufene Routine:
      Die DLL-Prozedur

      C-Quellcode

      1. EXPORT32 BOOL _stdcall SetStruct(TestStruct* st)
      2. {
      3. if(st == NULL)
      4. {
      5. return FALSE;
      6. }
      7. st->index++;
      8. if(st->index == 11)
      9. {
      10. st->index = 0;
      11. }
      12. if(st->text[0] != 0)
      13. {
      14. ::MessageBoxA(NULL, st->text, "Value to DLL", 0);
      15. }
      16. st->flag = !st->flag;
      17. st->testValue = a3;
      18. const char* text = "c:\\temp\\test.txt";
      19. strcpy_s(st->text, text);
      20. return TRUE;
      21. }

      Im Anhang ist das ursprüngliche und erweiterte VB-Projekt, dass um eine native C++-DLL zur Strukturübergabe
      sowie
      einem C#-Projekt erweitert wurde, in dem die Strukturübergabe demonstriert wird.
      Test_DLL_32.zip
      ---------
      Vielleicht ein kleines Schmankerl:
      In den Projekteinstellungen Debuggen gibt es den Schalter Nicht verwaltetes Codedebugging aktivieren
      Ist der aktiv, kann direkt von .NEWT aus in die C++-DLL hineingesteppt werden. :thumbsup:
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „RodFromGermany“ () aus folgendem Grund: Bild mit unmanaged-Debug-Schalter

      Rikudo schrieb:

      Ist es auch möglich mit einer 64-Bit C Dll zu komminizieren (Also per .net?)

      Wenn das .NET Programm im 64-Bit-Modus läuft (also Zielarchitektur x64 oder AnyCPU), sollte das gehen, ja. Wenn du in x64/AnyCPU .NET Programmen die Win32-API nutzt (die dann aber nur noch so heißt, im 64bit-System aber tatsächlich auch als 64bit-DLLs vorliegen) machst du im Grunde auch nix anderes.
      Weltherrschaft erlangen: 1%
      Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
      Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
      Danke.

      Rikudo schrieb:

      64-Bit C Dll
      Ja, kein Problem.
      Das geht sogar genau so wie mit einer 32-Bit-DLL, muss halt nur alles explizit getestet werden, das hab ich in diesem Beispiel nicht.
      Und das Studio muss explizit für das Erstellen von 64-Bit-C++-Anwendungen installiert werden (konfigurieren genügt nicht)!
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!

      RodFromGermany schrieb:

      Und das Studio muss explizit für das Erstellen von 64-Bit-C++-Anwendungen installiert werden (konfigurieren genügt nicht)!

      Huh ? VS hat min. 1 Plattformtoolset drin für x86, x64 und Itanium oder wie das hieß. Da muss ich nur auf x64 stellen und fertig
      @Gonger96 Mit VS 2010 musste ich meine Installation neu machen, um C++-64-Bit machen zu können, da ist iwo in den Properties exolizit ein Haken zu setzen.
      Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
      Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
      Ein guter .NET-Snippetkonverter (der ist verfügbar).
      Programmierfragen über PN / Konversation werden ignoriert!