C++ struct in C# nutzen
-
Hallo.
Ich schreibe eine Oberfläche für ein C Programm. Dabei lese ich Knotenkoordinaten ein und verarbeite diese. Das habe ich in einer C++ Bibliothek geschrieben. Die Koordinaten und weitere Elemente sind als Structs definiert.
Ich möchte nun von meinem C# Programm auf diese Elemente zugreifen. Ich habe das wie folgt realisiert:
C++ Code:
#ifndef STRUCTS_H #define STRUCTS_H #include "..\\..\\Header\\Structs.h" #endif #ifndef STDLIB_H #define STDLIB_H #include <stdlib.h> #endif #ifndef STDIO_H #define STDIO_H #include <stdio.h> #endif extern "C" __declspec(dllexport) int ReturnInt(int N){ return N; } extern "C" __declspec(dllexport) int Movenodes(double dx, double dy, TMesh** M){ unsigned long fi = 0; TMesh* fMesh = *M; for (fi = 0; fi < fMesh->nNode; fi++){ fMesh->pNode[fi].P.x += dx; fMesh->pNode[fi].P.y += dy; } for (fi = 0; fi < fMesh->nElement; fi++){ fMesh->pElement[fi].P.x += dx; fMesh->pElement[fi].P.y += dy; } return 0; } extern "C" __declspec(dllexport) int ReadMeshNastran(char* filename, int* nMesh, TMesh** M){ int fnMesh = *nMesh; int fi = 0; fnMesh++; *nMesh = fnMesh; printf("\n\nDLL-Output:\n\tFilename: %s\n\n", filename); TMesh* fM = (TMesh*)malloc(*nMesh*sizeof(TMesh)); fM->pNode = (TNode*)calloc(9, sizeof(TNode)); fM->pElement = (TElement*)calloc(12, sizeof(TElement)); fM->nAreaElement = 4; fM->nElement = 12; fM->nLineElement = 8; fM->pLineElement = &fM->pElement[fM->nAreaElement]; fM->nNode = 9; /*Lade Beispielnetz*/ //Node #1 fM->pNode[fi].P.x = -1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 1.; fM->pNode[fi].ExternalNumber = fi; //Node #2 fM->pNode[fi].P.x = 0.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 1.; fM->pNode[fi].ExternalNumber = fi; //Node #3 fM->pNode[fi].P.x = 1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 1.; fM->pNode[fi].ExternalNumber = fi; //Node #4 fM->pNode[fi].P.x = -1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 0.; fM->pNode[fi].ExternalNumber = fi; //Node #5 fM->pNode[fi].P.x = 0.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 0.; fM->pNode[fi].ExternalNumber = fi; //Node #6 fM->pNode[fi].P.x = 1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = 0.; fM->pNode[fi].ExternalNumber = fi; //Node #7 fM->pNode[fi].P.x = -1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = -1.; fM->pNode[fi].ExternalNumber = fi; //Node #8 fM->pNode[fi].P.x = 0.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = -1.; fM->pNode[fi].ExternalNumber = fi; //Node #9 fM->pNode[fi].P.x = 1.; fM->pNode[fi].Index = fi; fM->pNode[fi++].P.y = -1.; fM->pNode[fi].ExternalNumber = fi; fi = 0; *M = fM; return 0; }
Mit der Headerdatei für die Structs:
typedef struct TPoint{ double x, y; }TPoint; typedef struct TNode{ TPoint P; unsigned long Index; unsigned long ExternalNumber; struct TElement *EP; char NEP; }TNode; typedef struct TElement{ TPoint P; unsigned long Index; unsigned long ExternalNumber; TNode** Node; char nNode; struct TElement** E; }; typedef struct TMesh{ TNode* pNode; TElement* pElement; TElement* pLineElement; unsigned long nNode; unsigned long nElement; unsigned long nAreaElement; unsigned long nLineElement; };
Und darauf versuche ich nun in C# zuzugreifen:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using System.Runtime.InteropServices; // DLL support namespace CFS2D { unsafe static class Program { [DllImport("ReadMeshNastran_Win32_DLL.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern int ReturnInt(int N); [DllImport("ReadMeshNastran_Win32_DLL.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern int ReadMeshNastran([MarshalAs(UnmanagedType.LPStr)]String filename, int* nMesh, Structs_CS.TMesh** M); [DllImport("ReadMeshNastran_Win32_DLL.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern int Movenodes(double dx, double dy, Structs_CS.TMesh* M); [STAThread] static unsafe void Main() { Structs_CS.TMesh* pMesh = null; int nMesh = 0; string MeshFile = "Test.dat"; ReadMeshNastran(MeshFile, &nMesh, &pMesh); //Movenodes(1.5, 2.5, pMesh); int Count = 5; Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); Count += ReturnInt(30); Console.WriteLine("Return value:" + Count); Console.WriteLine("nMesh: " + nMesh); Console.WriteLine(""); Console.WriteLine(pMesh[0].nNode + " nodes output:"); for (ulong i = 0; i < pMesh[0].nNode; i++) { Console.WriteLine(" Node[" + i + "].Coordinate:" + pMesh[0].pNode[i].P.x + " / " + pMesh[0].pNode[i].P.y); } } } }
Mit der Klasse:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; // DLL support namespace CFS2D { unsafe static class Structs_CS { [StructLayout(LayoutKind.Sequential)] public struct TPoint{ public double x, y; }; [StructLayout(LayoutKind.Sequential)] public struct TNode{ public TPoint P; public ulong Index; public ulong ExternalNumber; public TElement* EP; public char NEP; }; [StructLayout(LayoutKind.Sequential)] public struct TElement{ public TPoint P; public ulong Index; public ulong ExternalNumber; public TNode** Node; public char nNode; public TElement** E; }; [StructLayout(LayoutKind.Sequential)] public struct TMesh{ public TNode* pNode; public TElement* pElement; public TElement* pLineElement; public ulong nNode; public ulong nElement; public ulong nAreaElement; public ulong nLineElement; }; } }
Wenn ich das Programm nun ausführe, so klappt das teilweise.
Return value: 35 wird zurückgegeben.
nMesh ist 1.
Die DLL schreibt "Test.dat" in die Konsole.Aber der Zugriff auf das Struct pMesh klappt nicht.
pMesh[0].nNode ist z.B. 51539607561Die ausgegebenen Knotenkoordinaten stimmen überhaupt nicht.
Es tut mir leid, wenn ihr bei meinem Programmcode Augenkrebs bekommt, aber das soll nur ein Test sein. Später werden die Koordinaten aus einer Datei geladen. Dafür gibts auch schon eine C-Routine, welche ich dann als DLL in C# nutzen möchte.Was mache ich falsch?
P.s.: Das kann natürlich auch Zufall sein, aber die ersten Koordinaten bei NodeOutput werden sogar mit 1 / -1 korrekt ausgegeben (keine Angabe von Gleitkommastellen).
-
Also, ich habe den code etwas erweitert. So würde es funktionieren:
Code:
extern "C" __declspec(dllexport) double NodeXCoord(unsigned long Index, TMesh* M){ return M->pNode[Index].P.x; } extern "C" __declspec(dllexport) unsigned long MeshNodeNumber(TMesh* M){ return M->nNode; } extern "C" __declspec(dllexport) double NodeYCoord(unsigned long Index, TMesh* M){ return M->pNode[Index].P.y; }
Und hier der code in C#:
Console.WriteLine(MeshNodeNumber(pMesh) + " nodes output:"); for (ulong i = 0; i < MeshNodeNumber(pMesh); i++) { Console.WriteLine(" Node[" + i + "].Coordinate:" + NodeXCoord(i, pMesh) + " / " + NodeYCoord(i, pMesh)); }
Das Ergebnis ist korrekt!
Das ist aber nicht so toll, wenn ich für jeden Wert eine Funktion benötige.
Ich hätte gerne direkten Zugriff auf die Daten und ich möchte sie auf keinen Fall zweimal im Speicher haben. Geht das?
-
Ich weiß nicht, ob man so komplizierte Strukturen komplett automatisch marshallen kann. Ich würde sagen, das geht nicht automatisch, aber alle Aussagen ohne Gewähr
Du brauchst wahrscheinlich Marshal.PtrToStructure. Die Strukturen musst du wohl nicht als unsafe deklarieren, für Zeiger kannst du IntPtr nehmen. Dann gehst in einer Schleife drüber und konvertierst die IntPtr in deine anderen Strukturen.
-
Danke, das wäre durchaus eine Alternative. Ich würde mir dann also einige Zeiger vom Typ IntPtr erzeugen und diesen dann z.B. die Adresse von den einzelnen Struct-Feldern (pNode, pElement, etc.) zuweisen.
Kann ich diese dann anschließend in C# mit einem "identischen" struct verknüpfen? Ich würde also das gleiche struct in C# definieren (oder kann man das C++ struct in der DLL nutzen?) um dann auch in C# unsafe über Zeiger auf die Daten in der DLL zugreifen zu können?
Unterm Strich wäre das genau das, was ich möchte. Ein natives C-Programm über C# Sharp zu steuern, von dort aus die Funktionen aufzurufen, welche die Daten im Speicher verarbeiten aber auch Zugriff auf die Felder des C-Programms zu haben um die aktuellen Werte graphisch in C# ausgeben zu können.Ich habe mich versucht:
[DllImport("ReadMeshNastran_Win32_DLL.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr Pointer2Nodes(Structs_CS.TMesh* M); [DllImport("ReadMeshNastran_Win32_DLL.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)] public static extern int ReadMeshNastran([MarshalAs(UnmanagedType.LPStr)]String filename, int* nMesh, Structs_CS.TMesh** M);
Die Definition von TNode in C#:
[StructLayout(LayoutKind.Sequential)] public struct TPoint{ public double x, y; }; [StructLayout(LayoutKind.Sequential)] public struct TNode{ public TPoint P; public ulong Index; public ulong ExternalNumber; public TElement* EP; public char NEP; };
Die Definition von TNode in der C++ DLL:
typedef struct TPoint{ double x, y; }TPoint; typedef struct TNode{ TPoint P; unsigned long Index; unsigned long ExternalNumber; struct TElement *EP; char NEP; }TNode;
Die Main aus C#:
Structs_CS.TMesh* pMesh=null; Structs_CS.TNode* pNode=null; IntPtr pNodeInt, pMeshInt; int nMesh = 0; string MeshFile = "Test.dat"; ReadMeshNastran(MeshFile, &nMesh, &pMesh); //Getting node pointer pNodeInt = Pointer2Nodes(pMesh); pNode = (Structs_CS.TNode*)pNodeInt.ToPointer(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); Console.WriteLine("nMesh: " + nMesh); Console.WriteLine(""); Console.WriteLine(MeshNodeNumber(pMesh) + " nodes output:"); Console.WriteLine(""); Console.WriteLine("C#: Adress of pNode[1].P.x: " + (int)&pNode[1].P.x); Console.WriteLine("C#: Adress of pNode[1].P.y: " + (int)&pNode[1].P.y); Console.WriteLine("C#: Groesse von Structs_CS.TNode: " + sizeof(Structs_CS.TNode)); Console.WriteLine(""); for (ulong i = 0; i < MeshNodeNumber(pMesh); i++) { Console.WriteLine(" Node[" + i + "].Coordinate:" + pNode[i].P.x + " / " + pNode[i].P.y); }
Der Code in der DLL:
extern "C" __declspec(dllexport) TNode* Pointer2Nodes(TMesh* M){ printf("DLL: Adress of Node[1].P.x: %i\n", &M->pNode[1].P.x); printf("DLL: Adress of Node[1].P.y: %i\n", &M->pNode[1].P.y); printf("DLL: Groesse von TNode: %i\n", sizeof(TNode)); return M->pNode; }
Naja, der Compiler verarbeitet das sogar...
Aber die Ausgabe ist wieder nicht korrekt.
`Node[0].Coordinate:-1 / 1 Node[1].Coordinate:1 / 2.12199579145934E-314 Node[2].Coordinate:4.24399158291868E-314 / 0
`
Interessant sind zwei Dinge:
1. Die erste Ausgabe stimmt sogar.
2. Die Adresse der weiteren Coordinatenpaare weicht ab.`
C#: Adress of pNode[1].P.x: 456454224
DLL: Adress of Node[1].P.x: 456454216
C#: Adress of pNode[1].P.y: 456454232
DLL: Adress of Node[1].P.y: 456454224
`
Und das macht auch Sinn, denn es deckt sich mit der unterschiedlichen Größe der Structs:
C#: Groesse von Structs_CS.TNode: 48
DLL: Groeße von TNode: 40Ich muss zugeben, dass ich mir bei der Materie noch sehr
unsicher bin und wie kann ich das Problem evtl. beheben?. Ich nutze für beide Teile des Programms den gleichen compiler, beide x64. Visual C++ 2013 Express und Visual C# 2013 Express.
-
Habe auch unsigned long (C++) mit ulong (C#) vergleichen. Ergebnis: 4 zu 8
Das gleiche beim char (C++) und char (C#): 2 zo 1Weshalb ist das so?
EDIT:
Ok, jetzt wirds ganz komisch.
Habe bei der C# Deklaration der structs alle ulong durch UInt32 ersetzt und
nun läuft es. Das mit den char, die in c# 2 Byte brauchen und in C++ nur 1 habe ich garnicht korrigiert. Klar, mit UInt32 und unsigned long wurde dieser Fehler korrigiert, aber der mit den Charaktern noch nicht. Wie kann das Programm jetzt funktionieren?using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; // DLL support namespace CFS2D { unsafe static class Structs_CS { [StructLayout(LayoutKind.Sequential)] public struct TPoint{ public double x, y; }; [StructLayout(LayoutKind.Sequential)] public struct TNode{ public TPoint P; public UInt32 Index; public UInt32 ExternalNumber; public TElement* EP; public char NEP; }; [StructLayout(LayoutKind.Sequential)] public struct TElement{ public TPoint P; public UInt32 Index; public UInt32 ExternalNumber; public TNode** Node; public char nNode; public TElement** E; }; [StructLayout(LayoutKind.Sequential)] public struct TMesh{ public TNode* pNode; public TElement* pElement; public TElement* pLineElement; public UInt32 nNode; public UInt32 nElement; public UInt32 nAreaElement; public UInt32 nLineElement; }; } }
-
Du meinst wohl
Das gleiche beim char (C++) und char (C#): 1 zu 2
?
Das funktioniert in deinen Strukturen bisher, weil das Alignment in den C bzw. C++ Strukturen hierfür sorgt, daß die folgenden Elemente auf geraden Adressen (bzw. standardmäßig sogar auf 4 bzw. 8 Byte) ausgerichtet werden.
Du solltest aber in C# statt 'char' den Datentyp 'byte' verwenden (denn in deiner C-Struktur stellt NEP ja eine Anzahl dar und kein Zeichen).
PS: Hast du das Marshalling denn selber erstellt? Du kannst auch den PInvoke Interop Assistant dafür verwenden (dieser erzeugt aber soviel ich weiß safe Code, d.h. basierend auf IntPtr und nicht auf unsafe-Zeiger).
-
Kennnt c# pointer?!?
[StructLayout(LayoutKind.Sequential)]
public struct TNode{
public TPoint P;public UInt32 Index;
public UInt32 ExternalNumber;public TElement* EP; //<= ?DAS GEHT?
public char NEP;
};
-
Ja, wenn man /unsafe bei den Compiler-Optionen einstellt.
Empfehlen würde ich das aber nicht, sondern stattdessen mit IntPtr arbeiten.
-
Hmm ohh ok.. ja würde da auch ehr IntPtr verwenden..