P/Invoke für Operatoren

  • C++

Es gibt 11 Antworten in diesem Thema. Der letzte Beitrag () ist von Gonger96.

    P/Invoke für Operatoren

    Hallo zusammen,

    ich melde mich auch mal wieder, diesmal mit einem für mich kniffligen Problem, für das ich nach zweitägiger Recherche immer noch keine Lösung gefunden habe.

    Es geht darum, in einer C# (also .NET) Anwendung Funktionen in einer unmanaged DLL aufzurufen. Ziel ist es, die Klassen einer unmanaged C++-DLL in .NET verfügbar zu machen.
    Hört sich erstmal nach einem Fall für P/Invoke an. Für mich als - wie ich dachte - alter Hase waren die ersten Schritte auch gar kein Problem. Auch, dass es um C++-DLLs ging, in denen auf Klassen und deren Methoden per P/Invoke zugegriffen werden sollte, war nicht wirklich problematisch. Wo ich jetzt seit zwei Tagen nicht weiterkomme, sind die Operatoren.

    Vorweg: Der "Umweg" über ein C++/CLI Projekt kommt aus diversen Gründen nicht in Frage. Ohne das jetzt aufdröseln zu wollen, nur kurz zusammengefasst: Die unmanaged DLL liegt sowohl in 32 als auch in 64 Bit vor, gewünscht ist eine(!) Wrapper-DLL, die für "AnyCPU" kompiliert ist, also eine reine CLR-DLL sein soll.

    Bisheriger Lösungsansatz ist dazu folgender:
    Die gesamte Klasse, um die es geht, wurde mit __declspec(dllexport) ausgezeichnet. Somit werden alle Member-Funktionen exportiert. Mit dem "dekorierten" Namen zwar, aber das ist zweitrangig, eine DEF-Datei kommt hier nicht in Frage, auf die Gründe dafür geh ich auf Zeitgründen nicht ein. Selbst die Konstruktoren und der Destruktor sind extern verfügbar sowie auch - sic! - die zur Klasse gehörenden Operatoren.

    Ein kleines Beispiel einer solchen C++-Klasse:

    C-Quellcode

    1. class __declspec(dllexport) CPunkt
    2. {
    3. public:
    4. double x;
    5. double y;
    6. CPunkt() { x = 0; y = 0; }
    7. CPunkt(double xx, double yy) { x = xx; y = yy; }
    8. void Set(double _x, double _y) { x = _x; y = _y; }
    9. const CPunkt operator+ (const CPunkt& other) const { return CPunkt(x + other.x, y + other.y); }
    10. }


    Eine Hilfsklasse, der ich den Namen einer DLL und den Funktionsnamen übergebe, holt mit diesen Informationen per GetProcAddress einen Zeiger auf die Funktion, der in einem statischen Delegaten zwischengespeichert wird. Hiermit kann ich bequem unterscheiden, ob ein 32- oder ein 64-Prozess vorliegt und entsprechend unterschiedliche DLLs (32 oder 64 Bit) sowie Funktionsnamen (insbesondere bei Pointern als Funktionsparameter unterscheiden sich die Namensdekorationen) berücksichtigen. Zum Speichern der Delegaten verwende ich ebenfalls eine eigene (statische) Klasse:

    C#-Quellcode

    1. [StructLayout(LayoutKind.Sequential, Pack = 8)] // Pack=8 ist korrekt, einfach ignorieren!
    2. internal unsafe struct __Punkt
    3. {
    4. public IntPtr* _vtable;
    5. public double x;
    6. public double y;
    7. }
    8. internal unsafe static class DLLCalls
    9. {
    10. [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    11. internal delegate int _ctor_void(void* ths);
    12. [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    13. internal delegate int _ctor_dbl_dbl(void* ths, double x, double y);
    14. [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    15. internal delegate int _Destructor(void* ths);
    16. [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    17. internal delegate void _void_ptr_dbl_dbl(void* ths, double d1, double d2);
    18. //[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
    19. //internal delegate void* _operator(void* a, void* b);
    20. //internal static _operator Punkt_OperatorPlus;
    21. internal static _ctor_void Punkt_Constructor_void;
    22. internal static _ctor_dbl_dbl Punkt_Constructor_dbl_dbl;
    23. internal static _Destructor Punkt_Destructor;
    24. internal static _void_ptr_dbl_dbl Punkt_Set;
    25. static DLLCalls()
    26. {
    27. if (!Environment.Is64BitProcess)
    28. {
    29. // 32-Bit
    30. }
    31. else
    32. {
    33. //Punkt_OperatorPlus = DLL.Call<_operator>("??HCPunkt@@QEBA?BV0@AEBV0@@Z");
    34. Punkt_Constructor_void = DLL.Call<_ctor_void>("??0CPunkt@@QEAA@XZ");
    35. Punkt_Constructor_dbl_dbl = DLL.Call<_ctor_dbl_dbl>("??0CPunkt@@QEAA@NN@Z");
    36. Punkt_Destructor = DLL.Call<_Destructor>("??1CPunkt@@UEAA@XZ");
    37. Punkt_Set = DLL.Call<_void_ptr_dbl_dbl>("?Set@CPunkt@@QEAAXNN@Z");
    38. }
    39. }
    40. }

    Man beachte die Aufrufkonvention ThisCall für die Delegaten, die es ermöglicht, auf Objekt-Member zuzugreifen, in dem der Zeiger auf das Objekt als erster Parameter übergeben wird.

    Die eigentliche Managed-Klasse beginnt dann folgendermaßen:

    C#-Quellcode

    1. public unsafe class MPunkt : IDisposable
    2. {
    3. private __Punkt* _ptr;
    4. private MPunkt(__Punkt* ptr)
    5. {
    6. _ptr = ptr;
    7. }
    8. public MPunkt()
    9. {
    10. _ptr = (__Punkt*)Memory.Alloc(sizeof(__Punkt));
    11. DLLCalls.Punkt_Constructor_void(_ptr);
    12. }
    13. public MPunkt(double x, double y)
    14. {
    15. _ptr = (__Punkt*)Memory.Alloc(sizeof(__Punkt));
    16. DLLCalls.Punkt_Constructor_dbl_dbl(_ptr, x, y);
    17. }
    18. public MPunkt(MPunkt other)
    19. {
    20. _ptr = (__Punkt*)Memory.Alloc(sizeof(__Punkt));
    21. Memory.Copy(other._ptr, _ptr, sizeof(__Punkt));
    22. }
    23. public double x
    24. {
    25. get { return _ptr->x; }
    26. set { _ptr->x = value; }
    27. }
    28. public double y
    29. {
    30. get { return _ptr->y; }
    31. set { _ptr->y = value; }
    32. }
    33. public void Set(double x, double y)
    34. {
    35. DLLCalls.TPunkt_Set(_ptr, x, y);
    36. }
    37. #region IDisposable Member
    38. ~MPunkt()
    39. {
    40. Dispose(false);
    41. }
    42. public void Dispose()
    43. {
    44. Dispose(true);
    45. }
    46. private void Dispose(bool bDisposing)
    47. {
    48. if (_ptr != null)
    49. {
    50. DLLCalls.Punkt_Destructor(_ptr);
    51. Memory.Free(_ptr);
    52. _ptr = null;
    53. }
    54. if (bDisposing)
    55. {
    56. GC.SuppressFinalize(this);
    57. }
    58. }
    59. #endregion
    60. }


    Das funktioniert wunderbar.

    Jetzt mein Problem: Ich finde keine Infos darüber und bekomme es nicht, den Delegaten für den im Beispiel vorhandenen +-Operator zu deklarieren und die Funktion in C# zu mappen.
    Ja, ich weiß, was die Funktion in der unmanaged DLL macht und könnte sie in C# einfach "nachbauen". Es geht mir aber ums Prinzip an sich, weil ich es in diesem Zusammenhang mit mehreren zig Klassen und mehreren hundert Funktionen zu tun habe, die evtl. zu einem späteren Zeitpunkt modifiziert oder ggf. korrigiert werden und der CLR-Wrapper exakt dasselbe tun soll wie die unmanaged DLL ohne separat mit-angepasst werden zu müssen. Das evtl. nötige nachträgliche Löschen oder Einfügen von Funktionen muss hier das "Höchste der Gefühle" bleiben. Kurzum: Ein Nachbauen der Operatoren ist absolut unerwünscht und wenn möglich zu vermeiden.

    tl;dr:
    Wie muss der oben auskommentierte Delegat für den +-Operator aussehen und wie sieht sinnvollerweise der Aufruf gemäß dem Schema der Wrapper-Klasse aus?
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.
    Sorry, aber die Antworten berücksichtigen nicht die besonderen Voraussetzungen, die ich im Startpost angedeutet habe. Ich dachte ich hätte deutlich gemacht, dass C++/CLI nicht in Frage kommt. Bitte erwartet nicht von mir, die Gründe dafür aufzuzählen. Ich werde es nicht tun.

    "ist quatsch" lass ich daher nicht gelten, weil am Thema bzw. der Fragestellung vorbei.

    "geht nur über xyz" wird von mir vorerst auch in Frage gestellt, da die Operator-Funktion als exportierte Funktion sichtbar ist und aufgerufen werden kann, mir aber aufgrund mangelnden Wissens über C++ nur nicht klar ist, wie der Plattform-Aufruf auszusehen hat.

    Selbst dass ich überhaupt die Möglichkeit habe, mithilfe eines Pointers auf ein Objekt als ersten Parameter auch Memberfunktionen per P/Invoke aufzurufen, verraten nur ganz wenige Sachbeiträge, die man an einer Hand abzählen kann und per Google nur findet, wenn man die richtige Kombination an Suchbegriffen wählt.
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.
    Das geht natürlich, da __thiscall immer so exportiert wird. Das Problem wird bei dir das Speichermanagement. Bei C++ schreibt man entweder einen Wrapper in C und greift per P/Invoke drauf zu, einen Wrapper in CLI und nutzt C++ interop oder benutzt COM Interfaces. Du kannst es natürlich gerne versuchen, guck dir die exportierte Funktion in meinem PEViewer (gucke Forum) an, der zeigt dir den Un- und dekorierten Namen an. Die rufst du dann einfach auf. Per P/Invoke oder GetProcAddress(), aber richtig und sauber ists nur über C++ interop. Imho auch schneller und besser wartbar als das was du machst.
    Das führt hier zu nix, wenn die Aufgabenstellung von niemandem vollständig gelesen wird.

    Kann den Thread bitte ein Moderator löschen, bevor er bei Leuten, die ähnliche Probleme haben und nach Lösungen suchen, in den Suchergebnissen auftaucht und ihnen doch nicht weiterhelfen kann.

    Danke.
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.

    Arby schrieb:

    Kann den Thread bitte ein Moderator löschen, bevor er bei Leuten, die ähnliche Probleme haben und nach Lösungen suchen, in den Suchergebnissen auftaucht und ihnen doch nicht weiterhelfen kann.
    Abgelehnt.

    Der Thread hier ist voll von Antworten, die genau dein Problem lösen. Wenn du kein C++/CLI verwenden willst, hat @Gonger96 in Post #5 das Vorgehen erklärt. Das hauptsächliche Problem bei P/Invoke liegt darin, dass die dekorierten C++-Namen keine validen C#-Bezeichner ergeben. Daher geht's nur per C++/CLI oder GetProcAddress.
    Mit freundlichen Grüßen,
    Thunderbolt
    Ja kann man über den EntryPoint machen, aber wie es aussieht macht er es über LoadLibrary/GetProcAddress, was ich sinnvoller finde, da man da besser mit 32/64 Bit zugleich umgehen kann. Außerdem kann man das name mangling dynamisch behandeln.

    Ich kann leider nur über das Mangling von gcc sprechen, keine Ahnung wie das bei MSVC aufgebaut ist und was für Tools da von Vorteil sind. Aber abgesehen davon sind operatoren auch nichts anderes als Funktionsaufrufe, also ich versteh immer noch nicht ganz wo dein Problem dabei ist.
    Wenn es um die Signatur geht, die kann ganz unterschiedlich sein, jenachdem wie du diesen eben in deiner C++ klasse defniert hast kann es ja z.b. auch by value sein.

    Das Pack=8 beim struct Layout stimmt nur für 64 Bit.

    Außerdem sei vorsichtig, dass deine DLL auch der Architektur deines Prozesses entspricht(dürfte man aber beim Laden bemerken).

    Anstelle von x/y über das klassenlayout zugreifbar zu machen solltest du evtl. getter und setter verwenden, da dies sowieso dem layout deiner C++ Klasse entsprechen sollte. Außerdem können vtables und data layout ziemlich Komplex werden, was den Aufwand erhöht.

    Die Frage ist wie gesagt ist es wirklich Sinnvoll C++ direkt zugreifbar zu machen oder nicht einfach eine C-Bridge zu verwenden, das ist der gängige Weg seit Jahren und wenn du moderner/besser sein willst, dann schreibe dir doch ein Tool, was dir die C Bridge erstellt.
    Ist aufjedenfall einfacher als direkt C++ zu verwenden(nicht mal Python kann das und die sind schließlich für ihre C-Bindings bekannt).

    C-Bridge löst das Problem mit name-mangling instant und compiler unabhängig. Im selben Zug wie du die C-Bridge erstellst kannst du auch die C# delegates/Klassen etc. erstellen lassen.
    Ich wollte auch mal ne total überflüssige Signatur:
    ---Leer---
    Die ganzen Probleme die @jvbsl genannt hat würdest du mit C++ Interop umgehen. Solange die C++ Klassen so einfach aufgebaut sind mags vielleicht noch gehen, aber sei dir bewusst, dass es absolut unsauber und nicht stabil ist. Es gibt übrigens noch DbgHelp, die gibt dir UnDecorateSymbolName etc.