Laden...

Problem mit DllImport, Strukturen und Strings

Erstellt von DaisyChain vor 8 Jahren Letzter Beitrag vor 8 Jahren 3.849 Views
D
DaisyChain Themenstarter:in
50 Beiträge seit 2010
vor 8 Jahren
Problem mit DllImport, Strukturen und Strings

Hallo!

Ich versuche gerade eine von mir erstellte C-Bibliothek an C# anzubinden. Was in Sprachen wie Java oder NodeJS wunderbar funktioniert, scheitert bei C# schon an den einfachsten Stellen. Grundlegen neige ich ja in C dazu für jede Struktur zwei Methoden zu erstellen, von denen eine den Speicherplatz reserviert, die Struktur mit Daten füllt und als Rückgabeparameter zurückgibt, die andere die die Struktur dann wieder freigibt.

Für C# habe ich das nun ein wenig geändert, so dass mein Code derzweit wie folgt aussieht:

Die C Struktur:


struct cdripper_drive {
	int drive;
	char *name;
	char *letter;
	bool readanalog;
	bool readc2;
	bool readcdda;
	bool isaccurate;
	bool readcdr;
	bool readcdrw;
	bool readcdrw2;
	bool readdvd;
	bool readdvdr;
	bool readdvdram;
	bool readisrc;
	bool readm2f1;
	bool readm2f2;
	bool readmulti;
	bool readsubchan;
	bool readsubchandi;
	bool readupc;
} cdripper_drive;

Dazu die Methode:


void cdripper_get_drive_info(int drive, struct cdripper_drive *info) {
    ...
}

Dann die Struktur in C#:


[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
    public struct cdripper_drive
    {

        /// int
        public int drive;

        /// char*
        [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]
        public string name;

        /// char*
        [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.LPStr)]
        public string letter;

        /// boolean
        public bool readanalog;

        /// boolean
        public bool readc2;

        /// boolean
        public bool readcdda;

        /// boolean
        public bool isaccurate;

        /// boolean
        public bool readcdr;

        /// boolean
        public bool readcdrw;

        /// boolean
        public bool readcdrw2;

        /// boolean
        public bool readdvd;

        /// boolean
        public bool readdvdr;

        /// boolean
        public bool readdvdram;

        /// boolean
        public bool readisrc;

        /// boolean
        public bool readm2f1;

        /// boolean
        public bool readm2f2;

        /// boolean
        public bool readmulti;

        /// boolean
        public bool readsubchan;

        /// boolean
        public bool readsubchandi;

        /// boolean
        public bool readupc;
    }

Und die dazugehörige Methode:



        /// Return Type: void
        ///drive: int
        ///info: cdripper_drive*
        [System.Runtime.InteropServices.DllImportAttribute(@"D:\Coding\workspace\cdripper\Debug\cdripper.dll", EntryPoint = "cdripper_get_drive_info")]
        public static extern void NativeGetDriveInfo(int drive, ref cdripper_drive info);

Frage ist nun, wie ich die beiden Strings richtig befüll, so dass diese auch in C# auslesbar sind. Die bisherige Problematik ist die:

  • Lasse ich die beiden Strings aus, bekommen die späteren bool-Parameter falsche Werte.
  • Reservier ich keinen Speicher für die Strings, sondern schreibe ich die Werte direkt hinein, so bekomme ich eine System.AccessViolationException.
  • Reservier ich den entsprechenden Speicher und schreibe dann die Werte hinein, so bekomme ich nach Start das Programmfenster nicht mehr zu sehen, Visual Studio gibt diesbezüglich aber auch keine Fehler aus.

Eine andere Alternative die ich nebenher ausprobiert hab war es sämtliche Parameter als Methodenparameter zu übergeben mit dem Resultat, dass die Strings da leer blieben, also keinen Inhalt hatten.

Hat jemand von Euch einen Tip wie ich dat Ganze nun zum laufen bring!?

Danke schonmal

4.931 Beiträge seit 2008
vor 8 Jahren

Hallo,

wie genau füllt denn deine C-Funktion die beiden String-Member der Struktur, d.h. reserviert diese dann auch den Speicherplatz dafür (mit malloc)?
Du kannst dir auch mal Marshaling between Managed and Unmanaged Code sowie Advanced Topics in PInvoke String Marshaling dazu anschauen.

Wenn die C-Funktion in einen übergebenen Speicher schreiben soll, dann verwendet man von C# aus den StringBuilder, wie in P/Invoke a Function Passed a StringBuilder.

PS: Wenn du einen CD-Ripper in C# implementieren möchtest, dann kannst du dir auch mal meine Beiträge (+ Links) dazu durchlesen:
[erledigt] CD von C# aus rippen
CD-Text einer AudioCD auslesen - WMPLib.dll (sowie CD-Text einer AudioCD auslesen - WMPLib.dll)
Audio-CDs schnell und qualitativ hochwertig in mp3 umwandeln

D
DaisyChain Themenstarter:in
50 Beiträge seit 2010
vor 8 Jahren

Natürlich reservier ich den Speicher mittels malloc. Mir kam nur kurz der Gedanke ob das bei ner übergebenen Struktur nötig wäre, daher nochmal nachgefragt. Aber eigentlich wäre das vollkommener Schwachsinn wenn ich das nicht tun müsste.

Ich hab jetzt mal eine Testmethode gebaut:


void cdripper_get_drive_name(int drive, char *name, int name_length) {

	...

	// Name
	if((name = malloc(strlen(drive_info.vendor)+strlen(drive_info.product)+strlen(drive_info.revision)+3)) == NULL) exit(1);
	strcpy(name, drive_info.vendor);
	strcat(name, " ");
	strcat(name, drive_info.product);
	strcat(name, " ");
	strcat(name, drive_info.rev);

	// Name length
	name_length = strlen(name);
}

Dazu die Definition


[System.Runtime.InteropServices.DllImportAttribute(@"D:\Coding\workspace\cdripper\Debug\cdripper.dll", EntryPoint = "cdripper_get_drive_name")]
        public static extern void NatvieGetDriveName(int drive, [Out]IntPtr name, [Out]int length);

Und eine Methode anhand dieses Beispiels, aus dem ich nun folgende Zeilen aufruf:


// Pointer
            IntPtr name_ptr = IntPtr.Zero;

            // Length
            int name_length = 0;

            // Call
            NatvieGetDriveName(drive, name_ptr, name_length);

            Console.WriteLine(name_length);

Resultat: Programmfenster öffnet sich nicht. Irgendwo scheint sich C# an dem malloc-Aufruf zu stören!?

Die Beispiele vondieser Seite lösen bei mir auch eine System.AccessViolationException aus!? Irgendwo muss der Hund doch begraben liegen...

Was den CD Ripper angeht: C# ist eigentlich gar nicht meine bevorzugte Sprache. Ich nutze es nur Aufgrund den GUI-Gestaltungsmöglichkeiten von WPF. Da ich den Code jedoch weiterverwenden möchte, und die zugrunde liegende Library eh in C geschrieben ist liegt natürlich nahe den Ripper in C zu schreiben und dann nach C# zu wrappen. 😉

W
872 Beiträge seit 2005
vor 8 Jahren

Vielleicht mixt Du 32 und 64 Bit Code.
Was ist Dein Platform-Target für .NET? Ich finde die "Prefer 32 bit" Einstellung als recht gefährlich.

D
DaisyChain Themenstarter:in
50 Beiträge seit 2010
vor 8 Jahren

Nope. Das Problem hatte ich am Anfang gehabt, hab diesbezüglich bei Visual Studio alles auf x64 gestellt. Die Librarys (inkl. meiner) sind ebenfalls für x64 gebaut.

W
872 Beiträge seit 2005
vor 8 Jahren

Du solltest in der Windows-Ereignisanzeige etwas sehen. Außerdem kannst Du mit richtigen Symbolen auch debuggen.
Normalerweise sollte auch einfach StringBuilder anstatt [Out]IntPtr funktionieren.

D
DaisyChain Themenstarter:in
50 Beiträge seit 2010
vor 8 Jahren

So, habe nun ein komplett neues, leeres Projekt gestartet, nachdem das Alte angefangen hat mich mit Exception zu bombardieren nachdem ich an den Ausnahmeeinstellungen herumgespielt hat und das Zurücksetzen nichts mehr gebracht hat. Im neuen Projekt hat der Debugger ein mal gestoppt mit einer Meldung, die ich nun auch in der Windows-Ereignisanzeige wiedergefunden habe:

Name der fehlerhaften Anwendung: Desktop.vshost.exe, Version: 14.0.22823.1, Zeitstempel: 0x55389b1d
Name des fehlerhaften Moduls: cygwin1.dll, Version: 2000.2.0.0, Zeitstempel: 0x00000000
Ausnahmecode: 0xc0000005
Fehleroffset: 0x00000000000e38e7
ID des fehlerhaften Prozesses: 0x28c0
Startzeit der fehlerhaften Anwendung: 0x01d08dfced522f4a
Pfad der fehlerhaften Anwendung: D:\Coding\C#\rpims\Desktop\bin\Debug\Desktop.vshost.exe
Pfad des fehlerhaften Moduls: C:\cygwin64\bin\cygwin1.dll
Berichtskennung: 23c41bc0-dd6c-422e-8efc-e8c415ab4859
Vollständiger Name des fehlerhaften Pakets:
Anwendungs-ID, die relativ zum fehlerhaften Paket ist:

Kann das sein, dass Windows die bzw. Visual Studio die DLL nicht mag, weil diese mittels cygwin kompiliert wurde, und es dadurch zu irgendwelchen Problemen kommt (wobei ich mich Frage was genau die cygwin1.dll mit meiner kompilierteen dll am Hut hat!?)!?

W
872 Beiträge seit 2005
vor 8 Jahren

Ich würde Visual Studio oder MinGW nehmen.
Ich finde gerade nicht mehr die genauen Flags für Cygwin, wenn Du tatsächlich darauf bestehst - dann musst Du aber auf jeden Fall eine statische DLL mit exports bauen .
Compile stand alone exe with Cygwin hat folgendes für Dich.

D
DaisyChain Themenstarter:in
50 Beiträge seit 2010
vor 8 Jahren

Ich besteh mit sicherheit nicht auf cygwin, MinGW als eigenständiger Compiler war mir bisher nur nicht allzubekannt, bzw. ich dachte dass cygwin diesen selber nutzt, jedenfalls hatte ich MinGW unter cygwin mitinstalliert gehabt.

Den -mno-cygwin flag mag der im übrigen nicht, sagt unrecognized command line option und kompiliert nicht.

Habe mir nun aber MinGW installiert, ein neues Projekt gestartet und damit die DLL gebaut, mit dem nun folgenden Resultat:


        [System.Runtime.InteropServices.DllImportAttribute(@"D:\Coding\workspace\cdr\cdr.dll", EntryPoint = "cdr_get_drive_name")]
        private static extern void NatvieGetDriveName(int drive, [Out]IntPtr name, [Out]int length);

Und der dazugehörigen Fehlermeldung:

Message=Der Einstiegspunkt "cdr_get_drive_name" wurde nicht in der DLL "D:\Coding\workspace\cdr\cdr.dll" gefunden.

Was klappt denn da jetzt wieder nicht!? Oo

Edit: Ok, einfach "extern "C" __declspec(dllexport)" vor die Funktion setzen. Damit findet C# dann auch die Methode innerhalb der DLL. Problem:

Genau das selbe wie mit dem cygwin build. Es funktioniert einfach nicht.

W
872 Beiträge seit 2005
vor 8 Jahren

Schau wieder nach einer Fehlermeldung, damit wir Dir helfen können.
Ich würde ausserdem mal eine ganz simple Variante der DLL ohne externe Abhängigkeiten erstellen und vielleicht mit einem C/C++ Programm die DLL mal testen.
Zum Verständnis - Cygwin ist eine komplette Umgebung/Shell, was Du dort kompilierst klappt erstmal nur innerhalb der Shell.

4.931 Beiträge seit 2008
vor 8 Jahren

Was mir noch einfällt, setz mal die CallingConvention auf Cdecl (voreingestellt ist Winapi, d.h. StdCall):


[DllImport(@"cdripper.dll", EntryPoint = "cdripper_get_drive_info", CallingConvention = CallingConvention.Cdecl)]
public static extern void NativeGetDriveInfo(int drive, ref cdripper_drive info);