Laden...

AccessViolationException... String aus c++ Array in C#

Erstellt von oskar27 vor einem Jahr Letzter Beitrag vor einem Jahr 1.019 Views
O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr
AccessViolationException... String aus c++ Array in C#

Hallo zusammen,

ich kämpfe mit einer "AccessViolationException" ("Es wurde versucht, geschützten Speicher zu lesen oder zu schreiben...") in meiner C#-Anwendung, die einen String aus einem Array aus einer DLL abrufen soll. Das string-Array wird von einer weiteren Anwendung gefüttert. Innerhalb jener Anwendung erfolgt der Zugriff auf das Array ohne Probleme (schreiben und abrufen).
Die DLL händelt auch noch int/double-Werte, die problemlos in beiden Anwendungen geschrieben und abgerufen werden. Nur string macht mir die Sorgen:

DLL-Methode:


LPSTR strArr[1000] = { "" };

LPSTR __stdcall GetStr(int gvId)
{
	if (gvId >= 0)
	{
		return strArr[gvId];
	}
	else
		return "0";
}

C#-Aufruf:


 [DllImport("C:\\Windows\\SysWOW64\\Alert.dll", CallingConvention = CallingConvention.StdCall)]
        public static extern string GetStr(int gvId);

string str = GetStr(i);

Wie kann ich den Zugriff auf diesen "geschützten Speicher" in C# realisieren? Vermutlich marshallen? Bin hauptsächlich in C# "zu hause". Es fällt mir schwer, in C++ zu arbeiten....
Auch eine Deklaration z.B. mit const char* (statt LPSTR) lässt die Fehlermeldung beim C#-Zugriff aufkommen, wobei die zweite Anwendung auch hiermit funktioniert und das const char* Array beschreibt und ausliest.

Besten Dank und Grüße
Oskar

S
248 Beiträge seit 2008
vor einem Jahr

Hallo Oskar,

sobald strings gemarshallt werden, würde ich den verwendeten CharSet im DllImport angeben.

Grüße
spooky

4.931 Beiträge seit 2008
vor einem Jahr

Du könntest auch noch zusätzlich

[return: MarshalAs(UnmanagedType.LPStr)]

angeben.

M
368 Beiträge seit 2006
vor einem Jahr

Die Verwendung von LPSTR deutet übrigens auf einen (sehr) alten C++-Code hin: difference-between-char-and-lpstr-in-windows

Goalkicker.com // DNC Magazine for .NET Developers // .NET Blogs zum Folgen
Software is like cathedrals: first we build them, then we pray 😉

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hallo spook, Th69 und M.L.,

vielen Dank! Das habe ich implementiert.
@M.L. ja, das kann auch per string Typ laufen, siehe unten const char*...
@spook, Th69: Die Deklaration im C#-Import sieht jetzt so aus:


[DllImport("C:\\Windows\\SysWOW64\\Alert.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPStruct)]
public static extern string GetStr(int system);

Die Fehlermeldung ist jetzt folgende (durch return: MarshalAs bedingt. CharSet-Angabe hat hierbei keine Änderung bewirkt):

Fehlermeldung:
Message = ""return value" kann nicht gemarshallt werden: Ungültige verwaltete/nicht verwaltete Typenkombination (Zeichenfolgenparameter müssen mit LPStr, LPWStr, LPTStr, BStr, TBStr, VBByRefStr oder AnsiBStr kombiniert werden).."

Die eine Anwendung kann string und LPSTR verarbeiten. Per string hatte ich in der DLL das Array als const char* Typ deklariert, was in dieser Anwendung wie auch LPSTR funktioniert.
Wäre der C#-Abruf per const char* einfacher zu händeln? (in der C#-Anwendung soll nur string-Abruf funktionieren, das Schreiben des Strings übernimmt ausschließlich die andere Anwendung).

DLL-Deklaration mit const char* Array:


const char* strArr[1000] = { "" };

const char* __stdcall GetStr(int gvId)
{
	if (gvId >= 0)
	{
		return strArr[gvId];
	}
	else
		return "0";
}

C#-Code:


[DllImport("C:\\Windows\\SysWOW64\\Alert.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern string GetStr(int gvId);

Fehlermeldung:
System.AccessViolationException
HResult=0x80004003
Nachricht = Es wurde versucht, im geschützten Speicher zu lesen oder zu schreiben. Dies ist häufig ein Hinweis darauf, dass anderer Speicher beschädigt ist.
Quelle = mscorlib
Stapelüberwachung:
at System.String..ctor(SByte* value)
at System.StubHelpers.CSTRMarshaler.ConvertToManaged(IntPtr cstr)
...

Was könnte noch helfen? Vielen Dank!

Gruß
Oskar

4.931 Beiträge seit 2008
vor einem Jahr

Vergleiche noch mal genau deinen und meinen Code...

Edit:
Ist bei GetStr(i) auch ein Wert vorhanden oder ist dieser null?
Und kommt der Fehler auch bei GetStr(-1) oder nur, wenn auf das String-Array zugegriffen wird?

das Schreiben des Strings übernimmt ausschließlich die andere Anwendung

Was genau meinst du damit? Hast du eine andere Anwendung, welche auch diese DLL nutzt? Dann hat aber jede Anwendung ihre eigenen Daten.

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hallo Th69,

Ja sorry, da hatte ich LPStr auch getestet, oben falsch gepostet. Die Fehlermeldung ist die bekannte:

Fehlermeldung:
System.AccessViolationException
HResult=0x80004003
Nachricht = Es wurde versucht, im geschützten Speicher zu lesen oder zu schreiben. Dies ist häufig ein Hinweis darauf, dass anderer Speicher beschädigt ist.
Quelle = mscorlib
Stapelüberwachung:
at System.String..ctor(SByte* value)
at System.StubHelpers.CSTRMarshaler.ConvertToManaged(IntPtr cstr)
....

Hast du eine andere Anwendung, welche auch diese DLL nutzt? Dann hat aber jede Anwendung ihre eigenen Daten.

Ja. Diese Anwendung A schreibt hauptsächlich das Array. Die Anwendung B (C#-Anwendung) soll neben float auch strings auslesen. float-Werte klappt einwandfrei. Die Daten aus der DLL können von beiden Anwendungen geschrieben und gelesen werden. Nur string klappt noch nicht.

GetStr(i) - i soll der Array-Index sein. -1 funktioniert daher nicht. Auf jedem Index ist ein String gespeichert. Das Speichern funktioniert, weil die Anwendung A diese schreibt und auch selbst auslesen kann. Die Anwendung B soll jetzt auch auf diesen String-Speicher von Anwendung A zugreifen können und per Array-Index diese abrufen können.

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hi Th69 nochmal,

bei -1 sollte "0" zurück gegeben werden. Hierbei stürzt Anwendung B ab und schließt ohne Exception.

4.931 Beiträge seit 2008
vor einem Jahr

Ich glaube, du verstehst nicht, wie DLLs in Anwendungen (Prozesse) eingebunden sind. Wenn mehrere Anwendungen auf dieselben Daten zugreifen wollen, dann benötigt man dafür Inter-Process-Communication (IPC) oder aber eine externe Datenhaltung (wie z.B. eine Datenbank). Oder ist das Array explizit als "shared data section" deklariert (s.a. Dynamic-link library: Memory management)?

Probiere mal


[DllImport("C:\\Windows\\SysWOW64\\Alert.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetStr(int system); // Rückgabetyp ändern

Und lass dir mal den zurückgegebenen Wert ausgeben (bzw. debuggen).

Wenn aber auch der Zugriff auf das String-Literal "0" nicht funktioniert, dann liegt evtl. noch ein anderes Problem vor.
Da du ja auf "SysWOW64" zugreifst, ist denn dein C#-Projekt explizit als "x86" (bzw. 32bit) eingestellt?

Edit: Bzgl. DLLs + Shared Memory habe ich noch folgenden Artikel Windows Libraries I: Weitere Informationen zu DLL's ... (Code s. "Shared Memory") gefunden.

M
368 Beiträge seit 2006
vor einem Jahr

im geschützten Speicher zu lesen oder zu schreiben

Funktioniert der ursprüngliche C/C-Code wie angedacht ? Wobei ein C oder C - Compiler (zumindest zur Kompilierzeit) idR ein liberaleres Verhältnis zum Manipulieren von Speicherinhalten hat.

Goalkicker.com // DNC Magazine for .NET Developers // .NET Blogs zum Folgen
Software is like cathedrals: first we build them, then we pray 😉

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hallo M.L.,

bis auf die STR-Option funktioniert alles wie angedacht. Compiler ist VS2019.

Gruß
Oskar

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hi Th69,

vielen Dank für diese Infos. Die C#-Anwendung ist auch eine 64Bit-Anwendung. Da die Pfade absolut eingestellt sind, macht es keinen Unterschied. Habe auch System32 getestet, mit dem gleichen Resultat. Aber stimmt, die 64-Bit-DLLs müssen ins System32 👍

Den IntPtr Tipp habe ich implementiert. Entweder nicht korrekt implementiert, oder es fehlt noch etwas am "Glück". Die Übersetzung in string gibt einen leeren string zurück. Ohne Fehlermeldung. In der DLL ist der Rückgabetyp LPSTR. Das passt vermutlich nicht?

C#-Code:


[DllImport("C:\\Windows\\System32\\ChartAlert.DLL", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetStr(int system);

private string IntPtrToStr(int i)
        {
            IntPtr ptr = IntPtr.Zero;
            ptr = GVData.GetStr(i);
            string results = Marshal.PtrToStringAnsi(ptr);
            return results;
        }

Gruß
Oskar

4.931 Beiträge seit 2008
vor einem Jahr

Ruf nun mal deine Methode IntPtrToStr mit den Werten -1, 0 und 1 auf?
Welchen Wert hat dann jeweils ptr sowie results?

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hey,

bei -1 gibt er den korrekten und vollständigen String aus, yeah. Supi! (habe in der DLL "Nulll" für diesen Fall eingetragen).
Bei ≥ 0 gibt er nur einen leeren String zurück. Der Zugriff auf die gespeicherten Strings im Array klappt also noch nicht. Die sind jedoch beschrieben. Die Anwendung A gibt die gespeicherten strings aus dem Array intern korrekt zurück.

Das gleiche Ergebnis (leerer String in C#-Anwendung) erhalte ich mit der Deklaration des strArrays und des Rückgabetyps der GetStr-Methode als const char*


#pragma data_seg( ".IPC" ) 

float valArr[MAXSYSTEM][MAXCAT][MAXPARAM] = { 0.0 };				//ValArray
const char* strArr[MAXSYSTEM] = { "" };							//StrArray

#pragma data_seg()

const char* __stdcall GetStr(int gvId)
{
	if (gvId >= 0)
	{
		return strArr[gvId];
	}
	else
		return "Nulll";
}

Gruß
Oskar

4.931 Beiträge seit 2008
vor einem Jahr

Das ist ja schon mal ein kleiner Fortschritt. Und jetzt gibt es wohl auch keinen Speicherzugriffsfehler mehr?

Das was mich jedoch wundert, ist, daß du für 1 einen Leerstring ("") und nicht null zurückerhältst - denn

const char* strArr[MAXSYSTEM] = { "" };

initialisiert ja nur den ersten Wert (Index 0) mit dem Leerstring, alle weiteren Werten werden auf NULL (bzw. nullptr) gesetzt.
Hat die C++ Anwendung auch nur einen Leerstring geschrieben? Und mit welcher Speicheranforderungsfunktion wird denn der String (const char*) erzeugt?

Was passiert denn, wenn du testweise mal das Array so initialisierst:

const char* strArr[MAXSYSTEM] = { "0", "1", "2" };

?

Ansonsten könntest du auch mal probieren, 2 C++ Anwendungen dazu zu erstellen (eine schreibt, die andere liest).

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hey,

dankeschön, ein neuer Schritt: mit der fixen Initialisierung rufen beide Anwendungen diese strings korrekt und vollständig ab. Ab nicht-initialisiertem Index wird null zurückgegeben. Klar soweit.
Jedoch: wenn Anwendung A jetzt einen der initialisierten Indizes neu beschreibt, ruft die C#-Anwendung wieder einen Leer-String ab. Anwendung A ruft den neu geschriebenen string korrekt ab. Unklar für mich.


#pragma data_seg( ".IPC" ) 

float valArr[MAXSYSTEM][MAXCAT][MAXPARAM] = { 0.0 };				//ValArray
const char* strArr[MAXSYSTEM] = { "test1", "test2", "test3"};		        //StrArray

#pragma data_seg()

Gruß
Oskar

4.931 Beiträge seit 2008
vor einem Jahr

Mir ist gerade aufgefallen, daß man mittels Zeiger keine Daten in verschiedenen Prozessen nutzen kann, s.a. How do I share data in my DLL with an application or with other DLLs?:

Each process gets its own address space. It is very important that pointers are never stored in a variable contained in a shared data segment. A pointer might be perfectly valid in one application but not in another.

Du müsstest also so etwas wie


char strArr[MAXSYSTEM][MAXLENGTH]

verwenden (also ein fixes 2D-Array). Und dann von C++ aus mittels str(n)cpy o.ä. die Texte reinkopieren.

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Dankeschön Th69 für deine Mühe. Es geht in kleinen Schritten voran🙂
Mit folgendem Code bekomme ich in C#-Anwendung etwas zurück, das jedoch noch nicht dem gespeicherten string entspricht. Ist das jetzt der rechte Weg und nur noch eine "Kleinigkeit" oder generell weiterhin Murks. Denn es erfolgt mit const char*.

Die Anwendung A speichert wie zuvor den string (der aus 1 bis maximal 4 Buchstaben besteht, vergaß ich noch zu erwähnen) per const char* und beschreibt ein statisches string array.
Auf das string array soll jetzt die C#-Anwendung zugreifen. Das könnte doch klappen, von der Idee her zumindest. Wo liegts jetzt noch im Argen?
Die Ausgabe hätte "TEST" lauten müssen.


#pragma data_seg( ".IPC" ) 

float valArr[MAXSYSTEM][MAXCAT][MAXPARAM] = { 0.0 };				//ValArray								
const char* strArrMC[MAXSYSTEM] = { "" };						//ArrayMC			
char strArrCA[MAXSYSTEM][STRLENGTH] = { "" };					//ArrayCA

#pragma data_seg()


#pragma region SET STR ->>wird nur von Anwendung A aufgerufen
float __stdcall SetStr(int gvId, const char* gvStr)
{
	if (gvId >= 0)
	{	
		strArrMC[gvId] = &gvStr[0];

		//FÜR CA
		int size = std::strlen(&gvStr[0]);
		memset(strArrCA[gvId], '\0', STRLENGTH);
		std::memcpy(strArrCA[gvId], &gvStr[0], size);
		//

		return 1.0;
	}
	else
		return -1111114.0;
}
#pragma endregion

#pragma region GET STR MC ->> wird nur von Anwendung A aufgerufen ->>fehlerfrei
const char* __stdcall GetStrMC(int gvId)
{
	if (gvId >= 0)
	{		
		return strArrMC[gvId];

	}
	else
		return "Nulll";
}
#pragma endregion

#pragma region GET STR CA ->> wird nur von C#-Anwendung aufgerufen -> nicht korrekte Zeichen aktuell (siehe screen)
const char* __stdcall GetStrCA(int gvId)
{
	if (gvId >= 0)
	{
		const char* myData = "";
		for (int j = 0; j < 5; j++)
		{
			myData += strArrCA[gvId][j];
		}
		return myData;

	}
	else
		return "Nulll";
}
#pragma endregion

C#-Aufruf an sich wie zuvor:


[DllImport("C:\\Windows\\SysWOW64\\Alert.DLL", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
 public static extern IntPtr GetStrCA(int system);

private string IntPtrToStr(int i)
        {
            IntPtr ptr = IntPtr.Zero;
            ptr = GVData.GetStrCA(0);
            string results = Marshal.PtrToStringAnsi(ptr);            
            return results;
        }

4.931 Beiträge seit 2008
vor einem Jahr

Sorry, aber dein C++-Code ist fehlerhaft:
Zum einen kopierst du in SetStr das Nullendezeichen ('\0') nicht mit und zum anderen erzeugt GetStrCA undefiniertes Verhalten (UB), da myData nur einen Leerstring als String-Literal darstellt und kein beschreibbares String-Array.

Wie schon geschrieben, benutze zum Kopieren eines C-Strings strcpy (bzw. strncpy mit Angabe der max. Länge).
Und hast du bei STRLENGTH auch dieses Nullendezeichen beachtet, d.h. dieser Wert sollte mindestens 5 sein?

Warum benötigst du denn noch das Array strArrMC (und wenn, dann sollte es nicht im Shared Memory Bereich liegen!)?

Hier mein (aus dem Texteditor) geschriebener Code dazu:


bool __stdcall SetStr(int gvId, const char* gvStr)
{
	if (gvId >= 0)
	{
		strArrMC[gvId] = gvStr;

		//FÜR CA
		std::strcpy(strArrCA[gvId], gvStr);
		// bzw.
		// std::strncpy(strArrCA[gvId], gvStr, MAXELNGTH); gvStr[MAXLENGTH-1] = `\0';

		return true;
	}

	return false;
}

const char* __stdcall GetStrCA(int gvId)
{
	if (gvId >= 0)
	{
		return strArrCA[gvId];
	}
	else
		return "Null";
}

(gvStr ist dasselbe wie &gvStr[0], jedoch kürzer und üblicher so zu schreiben)

PS: Irgendwie erzeugt der Browser aus >``= immer ein Zeichen - das mußt du dann wieder ändern.

O
oskar27 Themenstarter:in
12 Beiträge seit 2018
vor einem Jahr

Hey,

vielen Dank für deine abschließende Korrektur - es läuft jetzt! Klasse!!!
Es ist auch nur das char Array nötig und beide Anwendungen können jetzt drauf zu greifen. So simpel wie einfach😉))
Hab vielen Dank, Th69, für deine Tipps, die haben es auf den Weg gebracht und mir die Bäume aus der Sicht geräumt🙂

Hier der abschließende und funktionierende DLL- und C#-Code:

DLL-Code:


#define MAXSYSTEM 1000
#define STRLENGTH 4

#pragma data_seg( ".IPC" ) 										
char strArr[MAXSYSTEM][STRLENGTH] = { "" };
#pragma data_seg()

#pragma region SET STR
float __stdcall SetStr(int gvId, const char* gvStr)
{
	if (gvId >= 0)
	{	
		std::strcpy(strArr[gvId], gvStr);

		return 1.0;
	}
	else
		return -1111114.0;
}
#pragma endregion

#pragma region GET STR
const char* __stdcall GetStr(int gvId)
{
	if (gvId >= 0)
        {
		return strArr[gvId];
	}
	else
		return "Null";
}
#pragma endregion

C#-Code:


[DllImport("C:\\Windows\\SysWOW64\\Alert.DLL", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr GetStr(int system);

private string IntPtrToStr(int i)
{
        IntPtr ptr = IntPtr.Zero;
        ptr = GVData.GetStr(0);
        string results = Marshal.PtrToStringAnsi(ptr);            
        return results;
}

Gruß
Oskar

4.931 Beiträge seit 2008
vor einem Jahr

Puh 😉
Schön, daß es jetzt funktioniert und danke für die positive Rückmeldung.