Marshalling C <> C#
-
Hi zusammen,
ich versuche mich gerade am Marshalling von Objekten zwischen C und C# und komme da nicht weiter. Im Internet gibt´s tausende Beispiele zum Marshalling , aber mein Fall ist irgendwie nicht dabei. Ich habe eine DLL, die zwei Funktionen exportiert:alloc_test
undrelease_test
, dazu eine StrukturTest
:struct Test { int x; int y; }; // APICALL ist je nach Fall __declspec(dllimport) oder __declspec(dllexport) int APICALL alloc_test( Test** ptr ) { if( ptr ) { *ptr = (Test*) malloc( sizeof( Test ) ); (**ptr).x = 47; (**ptr).y = 11; } return ptr ? 1 : 0; } int APICALL release_test( Test** ptr ) { if( ptr ) { free( *ptr ); *ptr = NULL; return 1; } return 0; } // Anwendungsfall int main() { Test* ptr; alloc_test( &ptr ); ptr->x = 8; ptr->y = 15; release_test( &ptr ); }
Der Plan ist, die ganze Speicherverwaltung in der DLL zu machen, damit man sich nicht mit
CoTaskMem
rumärgern muss. der C# Part sieht bis jetzt so aus:namespace TestApp { [StructLayout(LayoutKind.Sequential)] struct Test { public Int32 x; public Int32 y; } // ab hier: kein Plan :/ internal static class CIntf { [DllImport("Test.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern Int32 _alloc_test( ... ); [DllImport("Test.dll", CallingConvention = CallingConvention.Cdecl)] internal static extern Int32 _release_test( ... ); } static void Main( string[] args ) { // wie sieht das Äquivalent zu Test* in C# aus? Bei Test* meckert der Compiler "Fehler CS0208 // 22 Es ist nicht möglich, einen Zeiger für den verwalteten Typ ("Test") zu deklarieren oder dessen // Adresse oder Größe abzurufen." Test t1; Test* t2; // der Aufruf sollte so aussehen und bei Erfolg t1/t2 auf das erzeugte Objekt zeigen lassen CIntf.alloc_test( t1 ); CIntf.release_test( t1 ); } }
Lässt sich das so lösen und wie sehen die Deklarationen aus? Hab schon div. Kombinationen mit Test, ref Test, Test* und ref Test* ausprobiert, aber der Compiler weigert sich jedes Mal mit einer anderen Fehlermeldung.
Oder ist die Idee, die Speicherverwaltung in der DLL zu halten schon Käse?
-
Das geht vermutlich schon irgendwie mit
unsafe
. Hab aber keinen Ahnung wie.Macht man aber eher nicht so.
Nackerte structs werden i.A. eher so gehandhabt dass man den Speicher auf der Aufrufer-Seite bereitstellt und die DLL bloss noch die Werte reinkopiert.Und wenn es um Objekte geht die irgendwelche Invarianten aufrechterhalten wollen/müssen, dann nimmt man dort eher undurchsichtige Zeiger.
Also eher so:
int APICALL foo_alloc( Foo** ptr ); int APICALL foo_release( Foo** ptr ); int APICALL foo_getTest( Foo const* ptr, Test* test );
Marshaling sollte dann trivial sein:
Foo**
->ref IntPtr
bzw.out IntPtr
Foo (const)*
->IntPtr
Test*
->ref Test
bzw.out Test
Wenn du die API speziell auf Interop mit .NET auslegen willst kannst du dir auch überleben als Return-Typ HRESULT verwenden. Dann kannst du die nämlich automatisch checken lassen wenn du willst, siehe https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.dllimportattribute.preservesig?view=netcore-3.1
-
ps:
@DocShoe sagte in Marshalling C <> C#:
int APICALL release_test( Test** ptr ) { if( ptr ) { free( *ptr ); *ptr = NULL; } return ptr ? 1 : 0; }
Copy-Paste Fehler, das
return ptr ? 1 : 0
ist hier Quatsch
-
@hustbaer sagte in Marshalling C <> C#:
ps:
@DocShoe sagte in Marshalling C <> C#:
int APICALL release_test( Test** ptr ) { if( ptr ) { free( *ptr ); *ptr = NULL; } return ptr ? 1 : 0; }
Copy-Paste Fehler, das
return ptr ? 1 : 0
ist hier QuatschGut aufgepasst, hab´s korrigiert
-
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 nochchar const*'
Member, die alsMarshalAs(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<...>
undboost::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 wiestd::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++ Funktionstd::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-stdvectorWobei 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(); }); }