Laden...

C-DLL in C#..

Erstellt von FiReFoX vor 18 Jahren Letzter Beitrag vor 18 Jahren 4.597 Views
F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren
C-DLL in C#..

Moin,

ich hab eine C-DLL kompiliert mit einer Funktion die ich von C# aus benutzen möchte.
Das wäre:

unsigned char *testFunc(unsigned char *argA, unsigned char *argB, int *argC)

Die export.def sieht folgendermaßen aus:

LIBRARY "TestLib"
EXPORTS testFunc @1

So weit so gut (kann leider nur sehr schlecht C, hoffe das reicht 😉 )

Hier mein C#-Code dazu:

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

namespace TestProg
{
    static class Program
    {
        [DllImport("TestLib.dll")]
        public static extern string testFunc(string argA, string argB, int argC);

        [STAThread]
        static void Main()
        {
            string a = "string1";
            string b = "string2";
            int c = 125;
            testFunc(a, b, c);

            // ....
        }
    }
}

Doch beim Aufruf kommt ein Fehler das ich versuche auf geschützen Speicher zuzugreifen. Könnt ihr Profis mir da bitte weiterhelfen?

Vielen Dank!

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo FiReFoX,

die Funktion erwartet einen int-Pointer keinen int. Bei deinem Aufruf versucht er daher auf Speicheradresse 125 zuzugreifen und das bringt den Fehler.

herbivore

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hi herbivore,

vielen Dank für deine Antwort. Kannst Du mir da bitte etwas mehr helfen?
Wenn ich int durch IntPtr austausche kommt trotzdem der gleiche Fehler.. 🙁

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo FiReFoX,

du musst wohl einen echten Pointer (also int* bzw. &c) verwenden und das geht nur in einem unsafe-Block.

herbivore

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hi herbivore,

Das klappt auch nicht.

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

namespace TestProg
{
    static class Program
    {
        [DllImport("TestLib.dll")]
        public static extern unsafe string testFunc(string argA, string argB, int* argC);

        [STAThread]
        static void Main()
        {
            string a = "string1";
            string b = "string2";
            string c;

            unsafe
            {
               int* c = (int*)125;
               c = testFunc(a, b, c);
            }

            // ....
        }
    }
}
G
68 Beiträge seit 2005
vor 18 Jahren

Also ich geh mal die Sache eher von der C Code Seite an:

unsigned char *testFunc(unsigned char *argA, unsigned char *argB, int *argC)

Du erwartest einen Pointer auf ArgA, ArgB, ArgC und gibst nen Pointer zurück.

In deinem C# Code sehe ich aber nur, dass Du einen Pointer erwartest aber nicht drei pointer auf a,b,c übergibst, sondern nur deren inhalt / wert.

ich könnte mir denken, dass der Aufruf eher so sein muss:

c = testFunc(&a, &b, &c);

und ganz oben:

public static extern unsafe string testFunc(string* argA, string* argB, int* argC);

Kann sein das ich mich irre!

Gruss,
Gregor W.

//edit

J
42 Beiträge seit 2006
vor 18 Jahren

Benutzen von C/C++ DLLs geht meiner Meinung nach IMMER ohne unsafe code und Verwendung von Pointern in C#

Dazu dieser Artikel auf MSDN

Kurze Zusammenfassung:
Du musst immer einen CLR Typ finden den du als Argument beim DLL Import angibst.
Wenn die C/C++ Funktion Pointer auf einen Datentyp erwartet, definiere den entsprechenden Parameter mit "ref" oder "out" keyword.
Speziell bei Zeichenketten: Wenn es ein konstanter Pointer auf eine Zeichenkette ist, also nur als eingabewert dient, benutze den CLR Typ string, ansonsten wenn er auch zur Ausgabe benutzt wird den StringBuilder.
Wie es sich da mit dem eigentlichen Rückgabewert via return verhält weiß ich nicht, ich vermute aber string sollte da in Ordnung gehen, da er ja komplett in der C-DLL angelegt wird.

Steht alles da drin ausführlich erklärt. Angewendet auf dein Beispiel ergibt das folgenden Code:

[DllImport("TestLib.dll")]
public static extern string testFunc(StringBuilder argA, StringBuilder argB, out int argC);

Wenn argA und argB nicht in der C Funktion verändert werden müsstest du auch ganz normal string benutzen können... aber auch ohne Garantie 😉

Außerdem lohnt sich gerade bei dem Thema ein Blick auf SharpDevelop.
Dort gibt es unter Extras "P/Invoke Signatur einfügen..." wo du die DLL und Funktionsname angibst, und er sagt dir die C# Definition der Funktion.

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Vielen Dank!
Kompiliert wurde mein Programm schonmal!
Ob die Ausgabe passt muss ich morgen noch testen 😉

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hi,

Ich hab jetzt die echte DLL kompiliert und nich die mit der TestFunc. Es handelt sich um einen Decoder für die Serverliste der GameSpy server (enctype2_decoder.zip)

Kompilieren unter C++ (VS.NET 2005) mit Exports.def klappt (mit oder ohne 'extern "C"', da C-Code).

Ich übergeben der Funktion einen String mit z.B. 40000 Zeichen, es müssten dann auch so circa wieder 40000 Zeichen zurückgegeben werden doch es sind zwischen 50 und 1000.

Sowohl Gregor's als auch Jabberwocky's Version klappt da nicht. Hab ich eventuell die DLL falsch kompiliert (mein C-Wissen is etwas bescheiden..)?

Grüße

51 Beiträge seit 2005
vor 18 Jahren

Ist die Rückgabe bzw sind die Texte leßbar??
Weil wenn \0 vorkommt ist klar, dass deine Texte alle dort abgeschnitten werden... weil pointer of array of char könnte auch was anderes sein 🙂

J
42 Beiträge seit 2006
vor 18 Jahren

Könntest es ja mal mit char[] als Rückgabewert versuchen und schauen was du da für ne Länge bekommst. Ich vermute auch es wird wegen \0 aufgehört.
könntest dann durchiterieren und alle \0 durch \n oder so ersetzen, und dann mit dem String Konstruktor umwandeln.

N
177 Beiträge seit 2006
vor 18 Jahren

Ich gehe mal davon aus, dass testFunc(argA, argB, argC) die Argumente nicht wirklich verändern will, sonder eben nur Pointer erwartet und dafür ein Ergebnis zurückliefert. Versuchs also mal hiermit:


using System;
using System.Collections.Generic;
using System.Text;

using System.Runtime.InteropServices;

namespace ConsoleApplication1
{
    class Program
    {
        [DllImport("TestLib.dll")]
        private static extern IntPtr testFunc(string argA, string argB, ref int argC);

        public static string MyTestFunc(string argA, string argB, ref int argC)
        {
    	    IntPtr i = testFunc(argA, argB, ref argC);
	        return Marshal.PtrToStringAnsi(i);
        }

        static void Main(string[] args)
        {
            string a = "string1";
            string b = "string2";
            int c = 125;
            string result =MyTestFunc(a, b, ref c);
        }
    }
}

Wenn argC tatsächlich verändert werden sollte, dann ersetze "ref" mit "out".

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hallo!

@Gigaseb & Jabberwocky:
Die Ausgabe is binär, möglich das auch 0x00 dabei ist.

@nop:
Danke ich werd mir den Code mal ansehen!

/ Edit: Klappt leider nicht. Es werden zwischen 0 und 800 Bytes zurückgegeben. Mittlerweile vermute ich das is die C-DLL wirklich falsch kompiliert hab oder so..

/ Edit 2: Vielleicht hilft das:

USAGE
\=====
Add the following prototype at the beginning of your code:

  unsigned char *enctype2_decoder(unsigned char *, unsigned char *, int *);

then use:

        pointer = enctype2_decoder(
            gamekey,        // the gamekey
            buffer,         // all the data received from the master server
            &buffer_len);   // the size of the master server

The return value is a pointer to the decrypted zone of buffer and
buffer_len is modified with the size of the decrypted data.
S
8.746 Beiträge seit 2005
vor 18 Jahren

IntPtr als Rückgabewert ist richtig. Da der Encoder aber sicherlich binär arbeitet, ist das Ergebnis byte[]. Statt PtrToStringAnsi() also Marshal.ReadByte(i, argC).

Ich würde davon ausgehen, dass auch die Eingangsdaten binär sind (auch hier wird eine Längeninfo verwendet), daher:

[DllImport("TestLib.dll")]
        private static extern IntPtr testFunc(byte[] argA, byte[] argB, ref int argC);
 

Bei Aufruf muss argC auf argB.Length gesetzt werden!

Du hast aber noch ein anderes, vermutlich schlimmeres Problem: Die Speicherfreigabe:

In deinem Fall allokiert die Routine Speicher und du - als Aufrufer - musst den Speicher freigeben. .NET hat Zugriff auf zwei "Speicher-Manager" (CoTaskMem und GlobalAlloc). Deine wird aber das malloc() der CRT nutzen, und das ist nicht mehr Betriebssystem (worauf .NET Zugriff hat), sondern C-Spezifisch. Normalerweise sollte eine Biblitiohek wie diese daher Free()-Routinen anbieten, wie z.B. FreeBuffer(pointer). So macht es Windows durchgängig.

Im Prinzip sollte die API ein paar Worte zur Freigabe verlieren. Ansonsten musst du testen und hoffen, dass dahinter ein Stück statischer Speicher steht (IntPtr wäre dann immer gleich), oder die Routine den Speicher funktions-intern statisch hält bzw. immer wieder neu allokiert und daher die Freigabe selbst macht (kannst du nur am Speicherverbrauch sehen).

Ich befürchte aber, du wirst Pech haben, und diese Routine nicht mit .NET nutzen können, eben wegen den Speicher-Problems. Wäre dann aber nicht deine Schuld, sondern eine amateurhaft programmierte Library. Eine Alternative wär noch ein managed C++-Wrapper, der hat Zugriff auf die CRT, wenn ich nicht irre.

N
177 Beiträge seit 2006
vor 18 Jahren

Svenson, die Byte-Infomation ist mir bei den ganzen vorherigen Postings mit Strings leider verlorengegangen. 🙁 Firefox, wenn du zumindest noch das Testprogramm zum laufen kriegen willst, dann solltest du alle "ref" durch "out" austauschen, da argC (wie du bereits geschrieben hast) hier die Länge zurückgibt.

S
8.746 Beiträge seit 2005
vor 18 Jahren

Ich denke, "Ref" bei der Länge ist schon richtig, weil es zum einen die Länge des Eingabefeldes, wie des Ausgabefeldes angibt.

J
42 Beiträge seit 2006
vor 18 Jahren

Original von svenson
IntPtr als Rückgabewert ist richtig. Da der Encoder aber sicherlich binär arbeitet, ist das Ergebnis byte[]. Statt PtrToStringAnsi() also Marshal.ReadByte(i, argC).

Ich würde davon ausgehen, dass auch die Eingangsdaten binär sind (auch hier wird eine Längeninfo verwendet), daher:

Also dass String für const char* und StringBuilder für char* klappt, dafür leg ich meine Hand ins Feuer. Das hab ich schon öfters getestet. Das Framework kümmert sich darum, dass die richtigen Daten ankommen. Man kann auch festlegen ob der Text Unicode oder Ansi ist, etc. Siehe den von mir gelinkten Artikel.

Nur mit Rückgabewerten hab ich keine Erfahrung. Bei dem was ich bisher gemacht hab kam die Rückgabe über nen Parameter und das return value war ein Error-Code (int)

IntPtr hab ich bisher nur für Handles aller Art benutzt...

S
8.746 Beiträge seit 2005
vor 18 Jahren

Das Ding ist doch, dass in C "char" auch für "byte" verwendet wird. Ob sowas wirklich ein C-style-String oder ein byte-Array gemeint ist (also Null-terminiert oder nicht), muss dokumentiert sein, oder ist aus der Signatur ablesbar. Ob es also "klappt" ist von der Intention, bzw. der Implementierung und des Eingangsdaten abhängig. Ein typisches Fehlersymptom wäre, dass zu wenige Daten konvertiert werden (Marshalling beendet beim erstbesten Null-Zeichen).

Nur weil du "string" verwendest, muss das also nicht abstürzen, u.U. arbeitet es nur nicht richtig.

Hier ist offenbar zumindest der zweite Parameter Längen-definiert, also damit KEIN String:

       buffer,         // all the data received from the master server  
        &buffer_len);   // **the size of the master server**  

Der erste Parameter scheint aber tatsächlich ein String zu sein, weil hier die Längeninfo fehlt.

Ergibt dann:

 
[DllImport("TestLib.dll")]
        private static extern IntPtr testFunc(string argA, byte[] argB, ref int argC);

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hallo,

erstmal vielen Dank das ihr meinen Problem Beachtung schenkt!!

[DllImport("TestLib.dll")]
        private static extern IntPtr testFunc(string argA, byte[] argB, ref int argC);

gibt auch fehlerhaften Daten zurück

So wie es aussieht wär eine kleine C++ Wrapper-Funktion die beste Lösung oder?

S
8.746 Beiträge seit 2005
vor 18 Jahren

Du wirfst aber die Flinte ganz schön schnell ins Korn! 🙂

Zeige doch noch mal den Aufruf der Funktion, vielleicht ist hier noch ein Fehler Hast du mal versucht mit dezidierten Testdaten zu arbeiten: (Array mit drei Bytes, dann noch ein Null dahinter, nächster Test mit einem Wert > 0 hinter der Null)?

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hallo!

Original von svenson
Du wirfst aber die Flinte ganz schön schnell ins Korn! 🙂

Liegt daran das ich sehr sehr wenig Ahnung von Pointern und C hab..
Ich will euch auch nich tagelang "auf die Nerven gehen".. 🙂

Zeige doch noch mal den Aufruf der Funktion, vielleicht ist hier noch ein Fehler Hast du mal versucht mit dezidierten Testdaten zu arbeiten: (Array mit drei Bytes, dann noch ein Null dahinter, nächster Test mit einem Wert > 0 hinter der Null)?

Bin gerade in der Arbeit aber wenn ich zuhause bin werd ich was posten!

Danke!
FiRe

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

So siehts bei mir jetzt aus:

[DllImport("GameSpy.dll")]
public static extern IntPtr enctype2_decoder(string key, byte[] data, ref int size);

private string DecodeServers(string serverList)
{
      string key = "d4kZca"; // GS Arcade key
      int size = serverList.Length;

      // Serverliste entschlüsseln
      IntPtr i = enctype2_decoder(key, Encoding.Default.GetBytes(serverList), ref size);
   
      // Entschlüsselte Servliste zurückgeben
      return Marshal.PtrToStringAnsi(i);
}

serverList sind eben die Daten die ich vom Socket empfangen hab.

N
177 Beiträge seit 2006
vor 18 Jahren

Benutzt Encoding.Default.GetBytes() auch die dll (oder eine andere)?
Wenn ja, dann brauchste nochmal eine extern declaration bzw. wrapper.
Byte[] oder string scheint wohl keine Rolle zu spielen, da immer string.

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hi nop.

Naja es wurde hier ja vorgeschlagen den zweiten Parameter als byte-Array festzulegen.
Und ich kenn kein andere Möglichkeit einen byte-Array aus nem String zu erstellen ausser Encoding.Default.GetBytes(). (Encoding.ASCII is ja 7-Bit oder?)

S
8.746 Beiträge seit 2005
vor 18 Jahren

Ungewöhnlich, dass die C-Funktion mit Unicode-Strings beschossen wird... sieht nicht korrekt aus. Du brauchst hier ANSI-Encoding (char *).

49.485 Beiträge seit 2005
vor 18 Jahren

Hallo svenson,

ohne, dass ich hier alles gelesen habe: Encoding.Default.GetBytes() liefert doch das Default-ANSI-Encoding.

herbivore

S
8.746 Beiträge seit 2005
vor 18 Jahren

Äh, korrekt....zulange CF programmiert.....

F
FiReFoX Themenstarter:in
32 Beiträge seit 2006
vor 18 Jahren

Hallo!

Mal ne Frage an die die sich den C-Code angeguckt haben: Ist es für mich möglich den Code nach C# zu portieren (nur geringe Pointer-Kentnisse)?
Eventuell durch unsafe C#-Code mit Pointern?