Marshalling C <> C#



  • Nach einiger Fummelei hab ich´s hinbekommen, ob man´s so macht oder nicht weiß ich nicht, aber immerhin funktioniert´s jetzt so wie gedacht.
    Die "echte" Testklasse hatte als Member noch char const*' Member, die als MarshalAs(UnmanagedType.LPStr) gemarshalled wurden. Damit war das C#-struct automatisch managed und kein Zugriff per Pointer möglich.

    [StructLayout(LayoutKind.Sequential)]
    public struct User
    {
       UInt64 ID;
       [MarshalAs(UnmanagedType.LPStr)]
       string Name;
       ...
    };
    

    Wenn ich das zu

    [StructLayout(LayoutKind.Sequential)]
    public struct User
    {
       UInt64 ID;
       Byte* Name;
    };
    

    umbaue habe ich zwar keinen komfortablen Zugriff auf die C-Strings im Objekt (Name, etc), aber ich habe direkten Zugriff auf den allokierten Speicher. Der Aufruf sieht dann so aus:

    namespace TestApp
    {
       internal static class CIntf
       {
          [DllImport("Test.dll", CallingConvention = CallingConvention.Cdecl)]
          internal static extern Int32 _query_user_by_id( IntPtr handle, UInt64 user_id, ref User* user );
    
          [DllImport("Test.dll", CallingConvention = CallingConvention.Cdecl)]
          internal static extern void _user_release( ref User* user );
       }
       
       void Main( string[] arg )
       {
          User* usr = null;
          int result = CIntf.query_user_by_id( some_handle, 4 /*user_id*/, ref usr );
          if( result != 0 && usr != null )
          {
             CIntf.user_release( ref usr );
          }
       }
    }
    

    Einzig die Strings muss ich jetzt selbst zusammenbauen, aber damit kann ich leben.



  • Also ich kann nur sagen ich würde das nicht so machen. Einfach schonmal weil du dazu unsafe brauchst (User*, Byte*).

    Und mir ist auch immer noch nicht klar wieso du Erzeugung/Freigabe in der native DLL mit direktem Zugriff auf die Member mischen willst.



  • Erstmal Danke für deine Zeit und Hinweise, hustbaer.

    Also.... wir haben eine C++ Bibliothek, mit der wir auf eine db zugreifen. Die Bibliothek hat etwas Logik und macht also mehr, also reine SQL-Statements an eine db abzusetzen. Sie wird hauptsächlich von Anwendungen benutzt, die ebenfalls in C++ programmiert sind und liefert Ergebnisse in der Form von std::vector<...>, std::map<...> und boost::optional<...> zurück.
    Wir möchten jetzt eine C# Anwendung bauen, die auf die C++ Bibliothek aufsetzt, damit wir die Logik für C# nicht neu programmieren müssen. Ab hier betreten wir Neuland, wir haben hier zwar Leute, die C# programmieren, aber eine Anbindung an C/C++ hat bisher noch niemand gemacht. Direktzugriff auf die C++ API scheidet wegen komplexer Datenstrukturen wie std::map oder std::string aus, weil da die Speicherinterna implementierungsabhängig sind.
    Der einfachste Weg schien mir jetzt, eine C-API auf die C++ Bibliothek aufzusetzen und dort die Daten so zu organisieren, dass man aus C# darauf zugreifen kann.
    Aus einer einfachen C++ Funktion

    std::vector<Something> query_something()
    {
    }
    

    wurde

    int query_something( Something** result )
    {
       try
       {
          std::vector<Something> data = query_something();
          if( !data.empty() )
          { 
             *result = new Something[data.size()];
             std::copy( data.begin(), data.end(), *result );
          }
          else
          {
             *result = nullptr;
          }  
          return 1;
       }
       catch( exception& excp )
       {
          ...
          return 0;
       }
    }
    
    void release_something( Something** data )
    {
        if( data )
        {
          delete[] *data;
          *data = nullptr;
        }
    }
    

    Hat natürlich den Nachteil, dass alle Daten für den Zugriff über die C-API kopiert werden müssen, was Besseres ist mir noch nicht eingefallen. Wenn ich deinen Vorschlag umsetzen möchte und den Aufrufer den Speicher allokieren lassen möchte muss der ja erst ein Mal wissen, wie viel Speicher er braucht. Also ähnlich der Windows API den ersten Zugriff um zu fragen, wie viele Elemente der Vektor hat und einen zweiten, um den Vektor zu kopieren. Dazu müsste ich mir den Vektor irgendwo merken, da ich die zu Grunde liegende db-Abfrage nicht zwei Mal ausführen möchte (oder darf).
    Wie sähe deiner Meinung nach eine solche Brücke zwischen C und C# aus?



  • Es gebe da noch die möglichkeit die .net erweiterung von C++ zu verwenden (AFAIK C++ CLR genannt)
    Um damit eine Wrapper klasse zu erzeugen, welche direkt von C# verwendet werden kann und intern die native C++ schnittstelle verwendet.
    Die wrapper klasse würde dann wohl meist nur die Daten von den unterschiedlichen Datenstrukturen umkopieren.

    Ein Beispiel um zwischen std::vector und einem System::array zu konvertieren
    https://stackoverflow.com/questions/6846880/how-to-convert-systemarray-to-stdvector

    Wobei das jetzt nur für einen primitiven datentyp ist. Bei einem struct/class müsste man hier entweder eigenen converter code schreiben oder es via der Marshaling funktionalität von .Net machen (wobei ich da keine Erfahrung habe in wie weit das möglich ist)



  • @firefly
    Leider nicht 😞
    Die C++ API benutzt, historisch bedingt, einige VCL Klassen, die für´s Visual Studio nicht zur Verfügung stehen. Eine Umstellung auf Standard-C++ zieht zu große Änderungen nach sich, die in mehreren Anwendungen grundlegenden Änderungen erfordern würden.

    Edit:
    Obwohl... das müsste ich mir mal genauer anschauen, ob das nicht vllt doch geht.



  • @DocShoe sagte in Marshalling C <> C#:

    Dazu müsste ich mir den Vektor irgendwo merken, da ich die zu Grunde liegende db-Abfrage nicht zwei Mal ausführen möchte (oder darf).
    Wie sähe deiner Meinung nach eine solche Brücke zwischen C und C# aus?

    So in der Art:

    struct WrappedSomething
    {
        std::vector<Something> data;
    };
    
    HRESULT query_something(WrappedSomething** result)
    {
        if (!result)
            return E_POINTER;
        *result = nullptr;
    
        try
        {
            auto wrappedSomething = make_unique<WrappedSomething>();
            wrappedSomething->data = query_something();
    
            *result = wrappedSomething.release();
            return S_OK;
        }
        catch (...)
        {
            return E_FAIL;
        }
    }
    
    HRESULT get_something_count(WrappedSomething const* s, size_t* count)
    {
        if (!count)
            return E_POINTER;
        *count = 0;
    
        if (!s)
            return E_POINTER;
    
        *count = s->data.size();
        return S_OK;
    }
    
    HRESULT get_something_item(WrappedSomething const* s, size_t index, Something* item)
    {
        if (!item)
                return E_POINTER;
        memset(item, 0, sizeof(*item));
    
        if (!s)
            return E_POINTER;
    
        if (index >= s->data.size())
            return E_INVALIDARG;
    
        *item = s->data[index];
        return S_OK;
    }
    

    Analog könnte man auch eine get_something_item_range Funktion machen wenn das Sinn macht.

    Wenn Something selbst Zeiger enthält wird es schwieriger. Dann muss man ggf. eigene Getter für die referenzierten Objekte machen.



  • @firefly sagte in Marshalling C <> C#:

    Es gebe da noch die möglichkeit die .net erweiterung von C++ zu verwenden (AFAIK C++ CLR genannt)

    Es heisst C++/CLI 😉



  • @hustbaer
    Sieht komisch aus und fühlt sich komisch an, aber ich probier das mal aus 😉



  • Hm, weiß nicht, das artet irgendwie aus. Statt einer Funktion brauche ich jetzt drei, und es gibt mehrere verschiedene Vektoren, da sind das ruck-zuck 20 zusätzliche Funktionen. Ich denke, ich bleibe bei der unsafe-Variante, aber die HRESULT Idee finde ich gut.



  • Wenn es nur unterschiedliche Vektoren sind, aber ansonsten alles gleich, kann man die Implementierung ja einfach als Template machen:

    HRESULT query_something(WrappedSomething** result)
    {
        return query_impl(result, [WrappedSomething& r](){ r.data = query_something(); });
    }
    

Log in to reply