Wie "managed" 2D-Array unter C++/CLI zugreifen ???



  • Hallo.

    Erstmal zur Erklärung: Mein Ziel besteht darin, einen C#-Wrapper für eine native C++ Klasse zu implementieren. Ich habe daher in C++/CLI eine "managed" Klasse geschrieben, die von der C#-Seite wie eine normale C#-Klasse aufgerufen werden kann und die die Aufrufe an die native C++ durch pumpt. Das klappt soweit auch alles. Die Frage ist nun allerdings, wie ich ein "managed" 2D-Array vom Typ** array<double,2> , das aus der C#-Anwendung herein kommt, in ein natives C Array vom Typ double** **umwandeln kann, um es an die native Methode durch zu reichen. Und zwar möglichst ohne alle Daten hin und her kopieren zu müssen 😉

    Bei einem 1D-Array geht das mit Hilfe eines** pin_ptr<T> **ohne Probleme, wozu man auch diverse Beispiele findet:

    void Native::bar(double *ptr);
    
    void Wrapper::foo(array<double> ^A)
    {
       pin_ptr<double> pinned = &A[0];
       double *ptr = pinned;
       native->bar(ptr);
    }
    

    Bei einem 2D-Array geht das aber nicht mehr, da ich hier den** pin_ptr<T> innerhalb der Schleife anlegen müsste. Allerdings hält der pin_ptr<T> **die Daten nur so lange fest, bis er "out of scop" geht, d.h. sofort am Ende des jeweiligen Schleifen-Durchlaufs werden die Pointer wieder ungültig:

    void Native::bar(double **ptr);
    
    void Wrapper::foo(array<double,2> ^A)
    {
        const int len = A->GetLength(0);
        double **ptr = (double**) alloca(sizeof(double*) * len);
        for(int i = 0; i < len; i++)
        {
             pin_ptr<double> pinned = &A[i,0]; //<-- Wird am Ende der Schleife "out of scope" gehen!
             ptr[i] = pinned;
        }
        native->bar(ptr); //Nicht sicher, da die Pointer bereits frei gegeben wurden!
    }
    

    Leider kann ich die** pin_ptr<T> Objekte nicht erhalten, da ein Array dieses Typs nicht erzeugt werden darf! Weder "managed" noch nativ. Auch in einen STL-Container lassen sie sich nicht speichern. Member-Variablen des Typs pin_ptr<T> **sind ebenfalls verboten...

    ______________________________________________

    Wie ich mittlerweile herausgefunden habe, lässt sich das Array auch mittels** GCHandle::Alloc(A, GCHandleType::Pinned) fest pinnen. Dann ist allerdings die Frage, wie ich über den GCHandle an die eigentlichen Daten heran komme. Anscheinend komme ich über handle.AddrOfPinnedObject().ToPointer() immerhin an einen nativen double* **Pointer heran. Durch Ausprobieren habe ich festgestellt, dass dieser Pointer auf einen Speicherbereich zeigt, in dem alle Daten des 2D Array's "flach" hintereinander liegen.

    Ich kann daher das native 2D-Array wie folge rekonstruieren:

    void Wrapper::foo(array<double,2> ^A)
    {
    	size_t dimOuter = A->GetLength(0);
    	size_t dimInner = A->GetLength(1);
    
    	GCHandle handle = GCHandle::Alloc(A, GCHandleType::Pinned);
    	double **ptr = new double*[dimOuter];
    	double *basePointer = reinterpret_cast<double*>(handle.AddrOfPinnedObject().ToPointer());
    
    	for(size_t d = 0; d < dimOuter; d++)
    	{
    		ptr[d] = basePointer;
    		basePointer += dimInner;
    	}
    
    	native->bar(ptr);
    
    	handle.Free();
    }
    

    Die entscheidende Frage ist nun, ob ich mich darauf verlassen kann, dass diese Methode immer funktioniert. Es erscheint mir doch eher wie eine Frickel-Lösung. Denn eine offizielle Dokumentation, wie die Daten, auf die** AddrOfPinnedObject().ToPointer() **zeigt, aufgebaut sind, konnte ich leider nicht finden. Es könnte also Implementationsabhängig sein. Der Code wurde übrigens unter .NET Framework 4.0 auf Windows 7 getestet...

    Kann jemand bestätigen, dass diese Methode "sicher" ist? Oder gibt es vllt eine elegantere Lösung, um an die Array-Daten zu kommen ???

    Danke im Voraus! 🙂



  • Hat tatsächlich noch niemand versucht von C++ aus auf ein "managed" 2D-Array zuzugreifen? 😕

    Ich habe mittlerweile herausgefunden, dass ein** pin_ptr<T> **auf das erste Element des ersten Sub-Arrays ebenfalls einen Zeiger auf die gesamten Daten des 2D-Arrays (in "serialisierter" Form) zu liefern scheint.

    Somit kann ich mir mein natives C++ 2D-Array letzten Endes mit einem einzigen** pin_ptr<T> zusammen basteln, so dass der pin_ptr<T> **nicht mehr in einer Schleife stehen muss und damit auch nicht "out of scope" geht:

    void Wrapper::foo(array<double,2> ^A)
    {
        const int64_t A_dimOuter = ARRAY->GetLongLength(0);
        const int64_t A_dimInner = ARRAY->GetLongLength(1);
        double **A_ptr = (double**) alloca(sizeof(double*) * ((size_t) A_dimOuter));
        pin_ptr<double> A_pinned = &A[0,0]; //Liefert Zeiger auf alle(?) Array-Daten
        double *basePtr = A_pinned;
        for(int64_t i = 0; i < A_dimOuter; i++)
        {
            A_ptr[i] = basePtr;
            basePtr += A_dimInner;
        }
        native->bar(A_ptr);
    }
    

    Die Frage ist aber weiterhin, ob ich immer davon ausgehen kann, dass die Daten aller Sub-Arrays einfach (in der richtigen Reihenfolge) direkt hintereinander im Speicher liegen... 🙄



  • DeathCubeK schrieb:

    Die Frage ist aber weiterhin, ob ich immer davon ausgehen kann, dass die Daten aller Sub-Arrays einfach (in der richtigen Reihenfolge) direkt hintereinander im Speicher liegen... 🙄

    Ja, kannst du. Weil array<double, x> den Speicher immer in einem Block hat. Es ist etwas anderes als etwa array<array<double>>, also ein Array von Arrays, wo die Speicherzellen irgendwo sein können.



  • /rant/ schrieb:

    Ja, kannst du. Weil array<double, x> den Speicher immer in einem Block hat.

    Okay, wenn das so ist, dann gut.

    Aber eigentlich hätte ich gerne eine Bestätigung, dass das .NET Framework bzw. das CLR dieses Verhalten tatsächlich garantiert. Letzten Endes ist ein "managed" System::Array ein Objekt unter Kontrolle des CLR und kann seinen Speichern intern verwalten wie es lustig ist, solange der [x][y]-Operator (die öffentliche Schnittstelle der Array-Klasse) wie erwartet funktioniert. Es sei denn, die .NET Spezifikation schreibt ausdrücklich vor, dass ein "managed" System::Array die Daten intern in einem einzigen Block ablegen muss. Ist das so?

    Leider finde ich in System::Array bzw. Array::CreateInstance überhaupt keinen Hinweis dazu... 😕



  • Hmm, laut C# Standard gilt folgendes:

    14.5.10.2 Array creation expressions

    [...]

    Except in an unsafe context (§27.1), the layout of arrays is unspecified.

    😡



  • Nun, viel mehr unsafe als C++/CLI und cli::pin_ptr geht ja wohl nicht mehr 😉

    Ausserdem: Wenns dir wirklich so wichtig ist, dann nimm halt ein unmanaged Array?



  • Mein Ziel besteht ja gerade darin, eine Wrapper-Klasse in C++/CLI zu schreiben, um die native C++ Bibliothek an das .NET Framework (primär C#) anzubinden.

    Das bedeutet, ich bekomme also aus der C#-Welt ein managed Array vom Typ** system::array<double,2> übergeben und muss das ganz irgendwie in einen nativen C/C++ Pointer vom Typ double** **umwandeln.

    Und zwar nach Möglichkeit ohne vor dem Aufruf der nativen Methode erstmal alle Daten aus dem managed Array in einen separaten Puffer kopieren zu müssen - und hinterher wieder zurück.

    /rant/ schrieb:

    Nun, viel mehr unsafe als C++/CLI und cli::pin_ptr geht ja wohl nicht mehr 😉

    Soweit ich das überblicke, existiert das "unsafe" Schlüsselwort nur auf der C#-Seite, jedoch nicht unter C++/CLI.

    Demnach müsste ich also meine C++/CLI Wrapper-Klasse auf der C#-Seite immer aus einem "unsafe" Context heraus aufrufen, damit sichergestellt ist, dass die Arrays mit definiertem "Layout" übergeben werden? 😕

    Eigentlich möchte ich ja nicht voraussetzen, dass meine Wrapper-Klasse nur aus einem "unsafe" Context heraus aufrufen werden darf...

    /rant/ schrieb:

    Ausserdem: Wenns dir wirklich so wichtig ist, dann nimm halt ein unmanaged Array?

    Ich denke nicht, dass das möglich ist, da die Daten ja ursprünglich aus der C#-Welt kommen und daher als managed Array nach C++/CLI übergeben werden...


Anmelden zum Antworten