String an C-Bibliothek übergeben



  • Hallo zusammen.

    Ich möchte über eine externe, mit nichtverwalteten Datentypen arbeitende C-DLL eine Datei in C# einlesen. Dazu wird der Dateiname als Parameter übergeben.

    In C# sieht das dann so aus:

    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 CSFD
    {
        unsafe public partial class frmMain : Form
        {
    
            [DllImport("ReadNASTRANMesh2.dll")]
            public static extern int ReadNastranMesh([MarshalAs(UnmanagedType.LPWStr)]String filename);
    
            public frmMain()
            {
                InitializeComponent();
            }
    
            unsafe private void meshToolStripMenuItem_Click(object sender, EventArgs e)
            {
                OpenFileDialog OFD = new OpenFileDialog();
    
                OFD.Multiselect = false;
                OFD.InitialDirectory = Application.ExecutablePath;
                OFD.Filter = "Nastran Mesh File (*.dat)|*.dat|All files (*.*)|*.*";
                OFD.FilterIndex = 0;
                OFD.Title = "Please select mesh file to open";
                if (OFD.ShowDialog() == DialogResult.OK)
                {
                    //Handling mesh file name
                    string MeshFileName = OFD.FileName;
    
                    //Executing library to read mesh file
                    int Returner = ReadNastranMesh(MeshFileName);
                    MessageBox.Show("Returned value:" + Returner);
                }
            }
        }
    }
    

    Die C-DLL sieht wie folgt aus und konnte auch erfolgreich kompiliert werden:

    #include <stdio.h>
    #include <string.h>
    #include <omp.h>
    
    extern "C"
    {
    	__declspec(dllexport) int ReadNastranMesh(char* filename)
    	{
    		printf("Reading NASTRAN mesh from %s\n", filename);
    		return 20;
    	}
    }
    

    Zugegeben, diese Funktion macht noch nicht besonders viel.

    Das ganze läuft fehlerfrei. Wenn ich das C#Programm ausführe, kann ich über den OpenFileDialog eine Datei auswählen und eine MessageBox mit "Returned value: 20" erscheint auch. Allerdings wird die printf Methode nicht korrekt ausgeführt bzw. ich denke, der Dateiname wird nicht korrekt übergeben. Sieht jemand etwas, was ich nicht sehe?

    Ich hätte noch eine Frage. Kann ich an die Methode in der DLL auch einen Zeiger auf ein Array von Klassenobjekten oder Structs übergeben?

    Wenn ich beispielsweise eine Klasse Element besitze definiere ich in C#:

    ulong nElement=1e6;
    Element[] ElArr = new Element[nElement];
    

    Wie müsste dann der DLL Import mit dem MarshalAs Befehl aussehen?

    Hintergrund ist: Ich möchte die Bibliothek verwenden, da ich das Einlesen und Verarbeiten der Daten parallelisieren will und auch den sicheren Umgang mit der Verwendung von DLL's aus C/C++ lernen möchte.



  • Also, ich hatte einen Fehler beim Einbinden der DLL. Wenn man es wie folgt macht, funktioniert es einwandfrei:

    C#-Code:

    [DllImport("ReadNASTRANMesh2.dll")]
            public static extern int ReadNastranMesh([MarshalAs(UnmanagedType.LPStr)]String filename);
    

    Die Frage mit der Zeiger auf ein Array von Klassenelementen steht noch im Raum.



  • Hi,
    ein verwaltetes Array kannst Du so an die native DLL übergeben:

    struct Element
    {
    	int x;
    };
    
    extern "C" __declspec( dllexport ) void func(Element* arr);
    void func(Element* arr)
    {
    	arr[0].x = 23;
    	arr[1].x = 42;
    	arr[2].x = 111;
    }
    
    struct Element
    {
    	int x;
    }
    
    static class Imports
    {
    	[DllImport("mydllname.dll", CallingConvention = CallingConvention.StdCall)]
    	public unsafe static extern void func(Element* arr);
    }
    
    unsafe void foo()
    {
    	Element[] arr = new Element[3];
    	fixed (Element* a = arr)
    	{
    		Imports.func(a);
    	}
    }
    


  • Allerdings würde ich die structs so definieren:

    #pragma pack(push, 1) // exact fit - no padding
    struct Element
    {
        uin32_t x;
    };
    #pragma pack(pop)
    
    [StructLayout(LayoutKind.Sequential)]
    public struct Element
    {
       public int x;
    }
    

    Das sollte dann Probleme durch Padding verhindern.

    Die Funktion in C würde ich als als

    extern "C"  __declspec(dllexport) void __stdcall func(int* arr);
    

    definerien, ich bin mir nicht sicher ob das immer automatisch stdcall ist..


  • Administrator

    Uff, in diesem Thread steht viel Unwissen und Mist...

    Ich wollte eigentlich ins Nest, muss morgen früh raus, aber das kann ich so einfach nicht stehen lassen. 😃

    DllImport("ReadNASTRANMesh2.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
    public static extern int ReadNastranMesh(String filename);
    
    • Wenn du CharSet = CharSet.Ansi setzt, dann wird der String automatisch als char* String übergeben.
    • ExactSpelling = true sichert, dass nicht zuerst nach ReadNastranMeshA gesucht wird. Beschleunigt den Aufruf etwas.
    • CallingConvention muss auf Cdecl gesetzt werden, wenn du eine normale C Funktion aufrufst. Ausser du hast etwas an den Kompilereinstellungen geändert und verwendest normalerweise die Aufrufkonvention StandardCall? Alle WinAPI Funktionen verwenden StandardCall und daher ist die Standardeinstellung auf CallingConvention.StdCall gesetzt. Eine normal exportierte C Funktion ruft man allerdings mit Cdecl auf.
    • Das MarshalAsAttribute ist nicht nötig.

    @DarkShadow44,
    Keine Ahnung woher du den Unsinn hast, dass man das Padding abschalten muss. Es ist genau umgekehrt. Du handelst dir damit Probleme ein. Das StructLayoutAttribute hat ein Property Pack . Standardmässig ist dieses auf 0 gesetzt, wodurch das übliche Padding/Alignment der Plattform verwendet wird.
    Eine exportierte C Funktion ist normalerweise Cdecl. Bei der WinAPI kommt das Makro WINAPI davor, welches zu einem __stdcall aufgelöst wird. Daher muss man WinAPI Funktionen mit StdCall aufrufen. Normale C Funktionen kann man mit Cdecl aufrufen.

    @μ,
    Wozu bitte den unsicheren Zeiger in C#? Zudem verwendest du kein StructLayoutAttribute !

    struct Element
    {
        int x;
    };
    
    extern "C" __declspec(dllexport) void func(Element* arr);
    
    void func(Element* arr, int count)
    {
        for(int i = 0; i < count; ++i)
        {
          arr[i].x = i;
        }
    }
    
    [StructLayout(LayoutKind.Sequential)]
    struct Element
    {
        int x;
    }
    
    static class Imports
    {
        [DllImport("mydllname.dll", EntryPoint = "func", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
        public static extern void Func([In, Out] Element[] arr, int count);
    }
    
    // ...
    
    void foo()
    {
        Element[] arr = new Element[3];
        Imports.Func(arr, arr.Length);
    }
    

    Das [In, Out] stellt sicher, dass geänderte Daten wieder an C# übergeben werden. Wenn du die Daten nur reingeben musst, dann ist es auch in Ordnung nichts anzugeben. Pass mit den Strukturen aber auf, es geht einfach mit einfachen Datentypen. Sobald es komplexere Datentypen benötigt, wird es bereits etwas aufwendiger. Was soll denn in diesen Element-Strukturen drin stehen?

    Grüssli



  • Keine Ahnung woher du den Unsinn hast, dass man das Padding abschalten muss. Es ist genau umgekehrt. Du handelst dir damit Probleme ein. Das StructLayoutAttribute hat ein Property Pack. Standardmässig ist dieses auf 0 gesetzt, wodurch das übliche Padding/Alignment der Plattform verwendet wird.

    Oh, ich bin davon ausgegangen dass bei "LayoutKind.Sequential" der "Pack" Wert automatisch 1 ist... 🙄
    War das laut C Standard nicht so dass das struct padding zwar meist immer gleich ist, aber trotzdem Implementationabhängig ist ?
    Somit müsste man doch, wenn man 100%ig sicher sein will, sowohl im C als auch im C# Code das Padding deaktivieren oder nicht ?


  • Administrator

    DarkShadow44 schrieb:

    Keine Ahnung woher du den Unsinn hast, dass man das Padding abschalten muss. Es ist genau umgekehrt. Du handelst dir damit Probleme ein. Das StructLayoutAttribute hat ein Property Pack. Standardmässig ist dieses auf 0 gesetzt, wodurch das übliche Padding/Alignment der Plattform verwendet wird.

    Oh, ich bin davon ausgegangen dass bei "LayoutKind.Sequential" der "Pack" Wert automatisch 1 ist... 🙄
    War das laut C Standard nicht so dass das struct padding zwar meist immer gleich ist, aber trotzdem Implementationabhängig ist ?
    Somit müsste man doch, wenn man 100%ig sicher sein will, sowohl im C als auch im C# Code das Padding deaktivieren oder nicht ?

    Es ist viel klüger sich darüber zu informieren, was der C-Kompiler genau gemacht hat. Schliesslich muss ein int auch nicht immer 32 Bit sein, was aber hier auch angenommen wird. Die Leute hinter dem .Net Framework haben sich aber schon etwas überlegt. Das Alignment wird ja standardmässig der CPU Architketur angepasst. Daher macht dieser Wert so schon Sinn, es wird die Mehrzahl der Fälle treffen.

    Grüssli



  • Hallo.

    Erstmal vielen vielen Dank für die reichlichen Antworten. Habe mich jetzt erstmal durch die Begriffe gewühlt und diese gegoogelt. Ich bin auch gut weiter gekommen... aber leider noch nicht ans Ziel. Anbei mein Code:

    CElement.h

    typedef public struct TPoint{
    	double x;
    	double y;
    	double z;
    };
    
    typedef public struct TVector{
    	double vx;
    	double vy;
    };
    
    typedef public struct TNode{
    	TPoint P;
    
    	unsigned long External_Number;
    
    	unsigned long* EP;
    	unsigned int nEP;
    };
    
    typedef public struct TEdge{
    	TNode *N1;
    	TNode *N2;
    
    	double length;
    };
    
    typedef public struct TFace{
    	TNode *N;
    	unsigned int nNode;
    
    	TVector Normal;
    
    	double Area;
    	unsigned int Family;
    };
    
    public class CElement{
    public:
    	CElement();
    
    	void CalculateNormalVector();
    	void CalculateVolume();
    	double ElementVolume();
    
    	TNode *Node;
    	unsigned long nNode;
    
    	TFace *Face;
    	unsigned long nFace;
    
    	unsigned int Family;
    
    private:
    	double Volume;
    };
    

    CElement.cpp

    #include <stdio.h>
    #include <string.h>
    #include "CElement.h"
    
    CElement::CElement(){}
    
    void CElement::CalculateVolume(){
    	//Code to calculate volume of element
    }
    
    void CElement::CalculateNormalVector(){
    	//Code to calculate normal vector
    }
    
    double CElement::ElementVolume(){
    	return Volume;
    }
    

    Dann gibt es die C++ DLL, welche Daten (Knotenkoordinaten und Elemente mit deren Knoten) aus einer Datei einlesen-, diese in die jeweiligen Arrays (CElement, TNode, etc.) legen- und dann diese zurückgeben soll. Die Funktion liest jetzt erstmal nichts ein, sondern erzeugt zwei Knoten und zwei Elemente. Es geht hier nur ums Prinzip.

    CReadMesh.cpp

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <omp.h>
    #include "Debug\CElement.h"
    
    extern "C" __declspec(dllexport) int ReadNastranMesh(char *filename, CElement **pElement, TNode **pNode, unsigned long *nElement, unsigned long *nNode){
    	unsigned long fnNode = 2; //Zwei Knoten
    	unsigned long fnElement = 2; //Zwei Elemente
    
    	//Speicher für Knoten und Elemente reservieren
    	TNode *fpNode = (TNode*)calloc(fnNode, sizeof(TNode));
    	CElement *fpElement = (CElement*)calloc(fnElement, sizeof(CElement));
    
            //Erzeuge zwei Knoten und zwei Elemente
    	fpNode[0].EP = (unsigned long*)calloc(1, sizeof(unsigned long));
    	fpNode[0].EP[0] = 0;
    	fpNode[0].EP[1] = 1;
    
    	fpNode[0].nEP = 2;
    
    	fpNode[0].External_Number = 0;
    
    	fpNode[0].P.x = .5;
    	fpNode[0].P.y = .6;
    	fpNode[0].P.z = .7;
    
    	fpNode[1].EP = (unsigned long*)calloc(1, sizeof(unsigned long));
    	fpNode[1].EP[0] = 1;
    	fpNode[1].EP[1] = 0;
    
    	fpNode[1].nEP = 2;
    
    	fpNode[1].External_Number = 1;
    
    	fpNode[1].P.x = -.5;
    	fpNode[1].P.y = -.6;
    	fpNode[1].P.z = -.7;
    
    	fpElement[0].nNode = 2;
    	fpElement[0].Node = (TNode*)calloc(2, sizeof(TNode));
    	fpElement[0].Node[0] = fpNode[0];
    	fpElement[0].Node[1] = fpNode[1];
    
    	fpElement[1].nNode = 2;
    	fpElement[1].Node = (TNode*)calloc(2, sizeof(TNode));
    	fpElement[1].Node[0] = fpNode[1];
    	fpElement[1].Node[1] = fpNode[0];
    
            //Ergebnisse zurück geben
    	pElement = &fpElement;
    	pNode = &fpNode;
    	nNode = &fnNode;
    	nElement = &fnElement;
    
    	printf("Reading mesh complette!\n");
    
    	return 0;
    }
    

    Nun noch der Code aus C#. Ein einfacher CommandButton führt das ganze aus:

    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;
    
    namespace MyDLL_Executable
    {
        public partial class Form1 : Form
        {
            [DllImport("CReadMesh.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
            public static extern unsafe int ReadNastranMesh(string filename, CElement[] pElement, TNode* pNode, ulong nElement, ulong nNode);
    
            public Form1()
            {
                InitializeComponent();
            }
    
            private unsafe void button1_Click(object sender, EventArgs e)
            {
                CElement[] pElement = new CElement[1];
                string ExFilename = "C:/Test.dat";
                TNode *pNode=null;
                ulong nElement=0;
                ulong nNode=0;
                ReadNastranMesh(ExFilename, pElement, pNode, nElement, nNode);
    
                Console.WriteLine("Number of nodes: " + nNode);
            }
        }
    }
    

    Ich musste in den Compiler Einstellungen auf "Allow unsafe code" umstellen und die Prozessorarchitektur explizit auf X86 umstellen - das liegt vielleicht an der ungenauen Beschreibung der Datentypen.

    Wenn ich den Code jetzt ausführe, gibt er mir "Number of nodes: 0" (das stand ja als printf in der DLL) und "Reading mesh complette!" aus. Wenn ich zweimal auf den CommandButton klicke, schreibt er in exakt dieser Reihenfolge:

    Number of nodes: 0
    Number of nodes: 0

    Reading mesh complette!
    Reading mesh complette!

    Die Reihenfolge ist komisch. Eigentlich sollte ja zuerst das Gitter gelesen und dann die Ausgabe erfolgen. Und natürlich sollte "Number of nodes: 2" erscheinen.

    Sieht jemand etwas, was ich nicht sehe?

    Vielen Dank für Eure Geduld... ist leider ein etwas umfangreicheres Problem mit vielen Fehlermöglichkeiten.

    Um der Frage, warum ich das über den Umweg einer CPP DLL mache vorzugreifen - es müssen mehrere Millionen Elemente und Knoten eingelesen werden, sie müssen sortiert werden. Das Volumen des Volumenelements muss berechnet werden, die Flächeninhalte aller Flächen, deren Normalenvektoren und am Ende müssen alle Knoten an der jeweiligen Fläche im mathematisch positiven Sinne (rel. zum Normalenvektor) angeordnet werden. Deshalb möchte ich die Routine parallelisieren. Ich weiß, dass das auch irgendwie in C# geht, aber im weiteren Programmverlauf gibt es externe DLL's, die ich einfach verwenden möchte.

    Grüsse,
    CJens



  • Auffällig ist, dass C# bei der Nutzung von CElement und TNode keine Fehler ausgibt. Diese beiden Typen wurden durch die DLL CReadMesh.dll mit eingebunden.

    Wenn ich jedoch nach der Ausführung schreibe: pElement[0].Family, dann kennt er
    dieses Attribut nicht. Er kennt also CElement, aber nicht das unsigned int Family. Wie kann das sein? Ich kann es ja denke ich auch nicht doppelt belegen.

    Wenn ich pElement[0]. schreibe, bietet er mir nur:
    -Equals
    -GetHashCode
    -GetType
    -ToString, also die üblichen Verdächtigen an.

    Zumindest die als public definierten Variablen nNode und nFace müsste er doch anbieten, oder?



  • @Dravere
    Danke für die Aufklärung 🙂

    Ich musste in den Compiler Einstellungen auf "Allow unsafe code" umstellen und die Prozessorarchitektur explizit auf X86 umstellen - das liegt vielleicht an der ungenauen Beschreibung der Datentypen.

    Unsafe Code brauchst du weil du (unnötig) Pointer in C# verwendest. Und wenn die DLL x86 ist muss dein C# Programm logischerweise auch x86 sein, mischen von x86 und x64 Code geht nun mal nicht (zumindest nicht ohne böse Hacks).

    Um der Frage, warum ich das über den Umweg einer CPP DLL mache vorzugreifen - es müssen mehrere Millionen Elemente und Knoten eingelesen werden, sie müssen sortiert werden. Das Volumen des Volumenelements muss berechnet werden, die Flächeninhalte aller Flächen, deren Normalenvektoren und am Ende müssen alle Knoten an der jeweiligen Fläche im mathematisch positiven Sinne (rel. zum Normalenvektor) angeordnet werden. Deshalb möchte ich die Routine parallelisieren

    Das ist kein Grund eine externe DLL zu nutzen, parallelisieren in C# geht einfacher. UNd komm mir jetzt bitte nicht mit "C# ist langsamer als C" oder so...
    Was die externen DLLs angeht: Welche brauchst du denn ? Vielleicht gibts ne Alternative oder gar nen Wrapper.

    Auffällig ist, dass C# bei der Nutzung von CElement und TNode keine Fehler ausgibt. Diese beiden Typen wurden durch die DLL CReadMesh.dll mit eingebunden.

    Allerdings das ist seltsam. Meiner Meinung nach sollte das aber einen Fehler geben weil sie nirgendwo definiert sind, aus der DLL kann das nicht geladen werden. 😮
    Vielleicht eine Art dynamische Typisierung... ? 😕

    Ich würde dir ernsthaft raten den ganzen "externe DLL" Mist wegzulassen und es entweder rein in C oder rein in C# einzubauen. Ich wäre für C#.
    Du scheinst nicht ganz zu wissen was du tust, und ja, das ist extrem Fehleranfällig! Zum Bespiel hast du massive Speicherprobleme, du forderst mit "calloc" an - wer gibt das frei ? Der GC ganz sicher nicht!



  • DarkShadow44 schrieb:

    Allerdings das ist seltsam.

    Wahrscheinlich C++/CLI, darauf deuten die ganzen "public class" sachen hin.

    Aber welche Sprache soll das sein ?

    public class CElement{
    

    und dann

    CElement *fpElement = (CElement*)calloc(fnElement, sizeof(CElement));
    


  • Wahrscheinlich C++/CLI, darauf deuten die ganzen "public class" sachen hin.

    Nein der Code ist eindeutig C#. C++/CLI hätte z.B. gcnew diese andere unschöne managed Syntax...

    Aber welche Sprache soll das sein ?

    Oh gut dass dus sagst, wäre mir jetzt gar nicht aufgefallen. Jetzt macht das ganz sogar halbwegs Sinn:
    Ich schätze die DLL ist nicht C, sondern C++/CLI. Dann verweist das C# Projekt auf die DLL (mit einem manged Verweis oder wie man das nennt) und lädt gleichzeitig die Funktionen über PInvoke.
    Das erklärt dann auch warum die Klassen in C# erkannt werden...
    Stimmt das so ? 😮



  • DarkShadow44 schrieb:

    Nein der Code ist eindeutig C#

    Er schreibt, die Datei sei "CElement.h", wie kann das C# sein ?

    DarkShadow44 schrieb:

    Stimmt das so ? 😮

    Ja, ich denke er mischt 4 Programmiersprachen ...



  • Er schreibt, die Datei sei "CElement.h", wie kann das C# sein ?

    Naja ein C Projekt (das vermutlich C++/CLI ist), und ein C# Projekt.



  • Komisches C 😕

    Wir haben da CElement

    public class CElement{
    public:
        CElement();
    
        void CalculateNormalVector();
        void CalculateVolume();
        double ElementVolume();
    
        TNode *Node;
        unsigned long nNode;
    
        TFace *Face;
        unsigned long nFace;
    
        unsigned int Family;
    
    private:
        double Volume;
    };
    

    und sind uns einig, dass es irgendwie managed sein muss, da es ja von C# aus der DLL geholt wird. Obwohl ich da ein ref oder value vor dem class erwartet hätte, wußte nicht, das das optional ist.

    Dann wird da das gemacht

    CElement *fpElement = (CElement*)calloc(fnElement, sizeof(CElement));
    

    was der Fragesteller anscheinend für das Anlegen eines Arrays von Objekten, managed oder unmanaged, hält. Wenn CElement aber ein managed type ist, warum lässt der Compiler dann CElement* zu ?



  • Es ist ein C# Projekt mit einer DLL aus C++. Die Zeile #include "CElement.h" ist im C++ Projekt zu finden, nicht im C# Projekt.

    Also, ich habe C/C++ programmiert, arbeite gerne mit Zeigern und würde das auch gerne in den DLLs aus C++ weiterhin so machen. In C# (darin habe ich wenig Erfahrung) kann ich darauf gerne verzichten. Deshalb aber der Mischmasch.

    Das Problem ist eine recht verzweigte Datenstruktur. Ich habe Elemente, die aus Knoten bestehen. Diese Elemente haben Oberflächen, die aus Knoten (den selben Knoten) bestehen. Die Oberflächen sind von Kanten umrundet, die wieder aus den selben Knoten bestehen. In meinem C-Programm habe ich ein Array mit allen Knoten erstellt. Dann habe ich noch einige Structs für TElement (Elemente), TFace (Oberflächen) und TEdge (Kanten). Diese Structs haben dann je ein Array von Zeigern auf Knoten. Das heißt, ich habe alle Knoten (die ja noch viel mehr Informationen enthalten und daher auch viel Speicher brauchen) EINMAL im Speicher. Die Elemente, Flächen und Kanten haben dann jeweils nur ein Array mit Zeigern auf die jeweiligen Punkte. Damit hab ich die Punkte nur einmal im Speicher - zeige aber überall, wo ich diese brauche, drauf. Diese Struktur hat sich sehr bewährt. Die Zeiger auf die Knoten ordnet man gerne so an, dass sie relativ zum Normalvektor links umlaufend angeordnet sind... dadurch kann man in Methoden mit Zeiger++ schneller durch das Array laufen.

    Das Problem ist ja, dass ich erst im Laufe des Einleseprozesses erfahre, wieviele Knoten ich habe, wieviele Elemente, welches Element aus wievielen und welchen Knoten besteht. Vorallen die Zeiger der Structs auf Arrays, die erst im Laufe des Programms aufgerufen werden und auf die mehrere Structs zugreifen sollen. In C++ habe ich das wie folgt gemacht:

    int main(){
       TElement *pElement=NULL;
       ReadMesh(filename, &pElement);
    
       return 0;
    }
    
    int ReadMesh(char* filename, TElement *pElement){
       unsigned long nElement=0;
    
       /*
       Das Programm sucht die Anzahl der Elemente und alle Knoten...
       Die Anzahl der Elemente wird in die Variable nElement geschrieben
       */
    
       TElement **fpElement = (TElement**)calloc(nElement, sizeof(TElement));
    
       /*
       Das Array fpElement wird jetzt gefüllt...
       */
    
       *pElement = fpElement; //Der Zeiger auf das erste Element von fpElement wird
                              //an pElement zurückgegeben
    
       return 0;
    }
    

    So wie der Code jetzt ist, kann ich natürlich auf die Klasse CElement verzichten und mit einem Struct TElement arbeiten. Aber später möchte ich diese Klassen noch um Methoden wie z.B. die Berechnung des Elementvolumens, Berechnung der Normalenvektoren etc. erweitern.

    Ich habe noch ein einfacheres Beispiel erstellt, welches natürlich auch nicht funktioniert:

    Hier die CElement.h

    struct TPoint{
    	double x;
    	double y;
    	double z;
    };
    
    class CElement{
    public:
    	CElement();
    	unsigned long ElementFamily = 0;
    };
    

    Hier die CElement.cpp

    #include "CElement.h"
    
    CElement::CElement(){};
    

    Hier die DLL_ReadMesh

    #include "CElement.h"
    #include <stdlib.h>
    #include <stdio.h>
    #include <math.h>
    #include <omp.h>
    
    extern "C" __declspec(dllexport) int ReadNastranMesh(char* filename, CElement **pElement, unsigned long *nElement){
    	unsigned long fnElement=2;
    
    	CElement *fpElement = (CElement*)calloc(fnElement, sizeof(CElement));
    
            /*Alternativ zur Erstellung des Arrays: CElement *fpElement = new CElement[fnElement];*/
    
    	fpElement[0].ElementFamily = 2;
    	fpElement[1].ElementFamily = 4;
    
    	*pElement = fpElement;
    	*nElement = fnElement;
    
    	return 0;
    }
    

    Und schließlich der Code des C# Programms:
    Die Klasse CSElement.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    public struct TPoint
    {
        double x;
        double y;
        double z;
    }
    
    namespace CS_Executable
    {
        // Class definition.
        public class CSElement
        {
            // Class members:
            // Property.
            public ulong ElementFamily { get; set; }
    
            // Instance Constructor.
            public CSElement()
            {
                ElementFamily = 0;
            }
        }
    }
    

    ...und der Code zur Ausführung des Programms:

    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;
    
    namespace CS_Executable
    {
        public unsafe partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            [DllImport("DLL_ReadMesh.dll", CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
            static extern unsafe int ReadNastranMesh(string filename, CSElement[] pElement, ulong nElement);
    
            private void button1_Click(object sender, EventArgs e)
            {
                ulong nElement = 2;
                CSElement[] pElement = new CSElement[2];
                Console.WriteLine("Executing DLL...");
                ReadNastranMesh("C:/Test.dat", pElement, nElement);
    
                Console.WriteLine("Family of first element: " + pElement[0].ElementFamily);
            }
        }
    }
    

    Naja, es ist ähnlich wie bei dem ausführlicheren Programm, dass CElement nicht korrekt zurückgegeben wird. Wenn ich vor der Ausgabe einen Holdingpoint einfüge ist das Array von CSElement immernoch mit zwei Einträgen und beide sind NULL.

    Es kam auch die Frage, warum ich diesen Umweg +ber DLL gehen möchte. Nun, erstmal weil es (auch wenn diese Antwort vorher schon als ungültig deklariert wurde) schneller ist. Zweitens muss ich in C# den Umweg über Delegates gehen, wenn ich OpenMPI implementieren möchte. Ich habe dazu ein Tutorial-Video... so einfach ist das nicht und in C++ weiß ich schon, wie es geht. Drittens, weil das Programm eigentlich schon fertig in C geschrieben ist - mir geht es hier um die Erstellung einer GUI (hier im Forum ist mir mehrfach ans Herz gelegt worden, C# für die Erstellung einer GUI zu nutzen und mein eigentliches Programm über DLLs zu importieren). Viertens: Weil ich eigentlich nur mit Pointers arbeiten kann. Ich möchte gewisse Daten nur einmal im Speicher haben, aber durch Pointer auf Arrays von Structs und dort auf einen gewissen Bereich, zugreifen können. Vielleicht geht das auch in C#, aber ich kann das nur in C++ :-/. Ich möchte möglichst viel I/O vermeiden. Theoretisch könnte ich ja in der C++ Bibliothek nur mit Structs arbeiten un diese dann, nachdem sie an C# zurückgegeben wurden in ein Array von Objekten des Typs CElement umwandeln.

    Und ich möchte die Boost Library (Bibliothek für numerische Mathematik) verwenden.



  • CJens schrieb:

    Also, ich habe lange C/C++ programmiert, arbeite gerne mit Zeigern und würde das auch gerne in den DLLs aus C++ weiterhin so machen.

    Was du da machst ist kein korrektes C++. Deswegen fällt es dir jetzt auch auf die Füsse. Man legt keine C++ Objekte mit calloc an. Deine calloc Aufrufe legen nur Speicher so groß wie die Objekte an.

    Und C# kann auf diese Weise keine C++ Klassen verwenden, nur C-Structs und -Funktionen. In deinem alten Beispiel muss da irgendwo C++/CLI im Einsatz gewesen sein.

    Die Beispiele zu structs am Anfang dieses Threads funktionieren so, dass die structs in C# dupliziert werden. Das ist aber sicher nicht, was du willst.
    Warum sollen die Daten überhaupt auf beiden Seiten sein ? Reicht es nicht von C++ ein C-Interface zu exportieren, mit dem man die Daten durchlaufen kann, z.B. um sie anzuzeigen ?



  • Hi.

    Naja, habe natives C, also prozesssorientiert programmiert - ohne Klassen. Mit Klassen habe ich vor nem Jahr in C# angefangen.

    Die Daten sollen nicht auf beiden Seiten sein. Die C++ Routine soll aus einer Datei Knotennummern mit den zugehörigen x,y und z Koordinaten auslesen. Außerdem stehen in dem File die Elemente. Also, Elementnummer, Knotennummer 1, Knotennummer i, ... , Knotennummer n-1, Knotennummer n.

    Also, hat das Element vier Knoten weiß ich, dass es ein Tetraeder, bei fünf eine Pyramide, bei sechs ein Prisma, bei acht ein Hexaeder und bei n Knoten ein Polyeder ist.

    Es soll eine Liste von Klassen CElement (Elemente) und dem struct TNode (Knoten) erzeugt werden. Diese Klasse und die structs sehen wie folgt aus:

    CElement.h

    struct TPoint{
       double x; //x coordinate
       double y; //y coordinate
       double z; //z coordinate
    }
    
    struct TVector{
       double nx; //normalized x direction
       double ny; //normalized y direction
       double nz; //normalized z direction
    
       double length; //length of the vector
    }
    
    struct TFace{
       TNode** Node; //Pointer to an array of pointer to the cornernodes of the face
       unsigned int nNode; //Index of the array Node
    
       double Area; //Area of the face
    
       TVector Normal; //Normalvector of the face
    }
    
    struct TNode{
       TPoint P; //Coordinates of the node
    
       unsigned int nEP; //Number of connected elements
       unsigned long** EP; //Array of index of connected elements
       double* Gamma; //Pointer to an array of interpolation coefficients
    }
    
    class CElement{
       public:
          public unsigned int nNode; //Number of nodes
          public unsigned int nFace; //Number of faces
          public double Volume; //Volume of the element
    
          public TPoint P; //Center of the element
          public TNode** Node; //Pointer to an array of pointers of nNode Nodes.
          public TFace** Face; //Pointer to an array of pointers to nNode Faces.
    
          public void CalculateVolume();
          public TPoint* CalculateCrossSection();
    

    Das Einlesen und Berechnen und sortieren all dieser Daten (am Anfang hat man ja nur Koordinaten und Knotennummern) soll mit dem C++ Code geschehen. Erst wenn die Daten fertig sind möchte ich sie, bzw. eine Referenz darauf an C# übergeben um dort damit weiter arbeiten zu können. Die Daten SOLLEN also auf jeden Fall nur einmal im Speicher liegen... es können viele Millionen Elemente und Knoten sein und mit dem Speicher hat man bei diesen Anwendungen ohnehin immer Probleme 🙂

    Mal ganz direkt gefragt: Geht das was ich da vorhabe überhaupt oder muss ich Umwege gehen? Würde ich z.B. nur Structs verwenden (also auch ein Struct für die Elemente), wäre das ganze wie ich verstehe nicht einfach, aber viel einfacher...

    Ich programmiere kein CLI. Als ich mit C++ anfing, habe ich mir ein Visual Studio C++ Buch gekauft und war schockiert, dass das alles total anders als in nativem C, das ich schon lange kannte, ist... und ich war sehr froh also ich herausfand (Wochen später), dass das kein natives C++, sondern C++/CLI ist.



  • Offtopic das Problem des OPs betreffend. Habe den Thread nicht mehr verfolgt und wollte nur Dravere was fragen:

    Dravere schrieb:

    @μ,
    Wozu bitte den unsicheren Zeiger in C#? Zudem verwendest du kein StructLayoutAttribute !

    Sequential ist default für Wertetypen.

    This enumeration is used with StructLayoutAttribute. The common language runtime uses the Auto layout value by default. To reduce layout-related problems associated with the Auto value, C#, Visual Basic, and C++ compilers specify Sequential layout for value types.

    http://msdn.microsoft.com/en-us/library/system.runtime.interopservices.layoutkind(v=vs.110).aspx

    Und warum Zeiger ... hmm, Gewohnheitssache und weil es sich manchmal nicht vermeiden lässt, ohne Daten zu kopieren.
    Wie greift man in sicherem (im Sinne des unsafe-Schlüsselwortes) Kontext auf Daten hinter einem IntPtr zu, ohne sie mit Hilfe der Marshal-Klasse zu kopieren?
    Konkretes Beispiel in OpenGL (OpenTK): Man kann mit GL.MapBuffer ein VertexBufferObject in den CPU-Speicher mappen und eben diese Methode liefert einen IntPtr. Auf eine Kopie über die Marshal-Klasse möchte ich verzichten und die Daten direkt bearbeiten bevor sie in die GPU zurückgemappt werden.. Mir ist dazu nur die unsafe-Variante inklusive Pointergefrickel bekannt. Wie auch sonst?
    Ernstgemeinte Frage, falls Du irgendwas dazu weißt, bitte raus damit. 🙂

    Grüße



  • CJens schrieb:

    dass das alles total anders als in nativem C, das ich schon lange kannte, ist... und ich war sehr froh also ich herausfand (Wochen später), dass das kein natives C++, sondern C++/CLI ist.

    Trotzdem hat dein jetziges Problem indirekt auch wieder damit zu tun. Deine ganzen Beispiele haben zwei grundlegende Probleme:

    Du benutzt C++ Klassen falsch. Wenn ein C++ Objekt angelegt wird, wird erstmal der Speicher angelegt und dann der Konstruktor ausgeführt. Beim Abräumen wird erstmal der Destruktor (bzw. die Destruktoren bei Arrays) ausgeführt, dann der Speicher freigegeben. Das ist was new und delete bzw. Smartpointer machen. Dein calloc legt nur Speicher an, was bei einfachen C++ Klassen zufällig funktioniert, weil das Speicherlayout von C++ Klassen in diesem Falle so aussieht.

    Den Fehler 1 überträgst du jetzt auch noch auf C#. Du legst eine Klasse CElement in C++ an und eine in C#. Dann glaubst du das wäre das gleiche und schiebst da Zeiger hin und her. Das Problem ist nur, dass die C# Klasse ganz anders aufgebaut ist, außerdem erbt sie implizit von System.Object. Sie liegt in einem ganz anderen, verwalteten, Speicherbereich, wo der Garbagecollector sie beim Aufräumen auch noch verschieben darf.

    Du mischt also verwaltete und nicht verwaltete Objekte und machst darauf mit deinem calloc rum. Damit überschreibst du irgendwo Speicher. Wo irgendwo ist, hängt wahrscheinlich von Compiler- und Netframeworkversion ab.

    Weiter helfen kann ich da nicht. Ich würde empfehlen, dass du bei C bleibst. Vergiss C++ Klassen und alles was mit .net zu tun hat. Mach deine GUI mit reiner WinAPI, oder arbeite an deinen grundlegenden Verständnis von C++ und C#, aber besser nicht gleichzeitig.


Log in to reply