Seite 1 von 2

[gelöst]C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 13:23
von joggel
Hallo ZFXler,

ich stehe hier vor einem Problem.

Ich möchte ja einen Teil einer C++-Bibliothek nach C# portieren.
Ich habe mir dazu einen Wrapper in C++/Cli geschrieben, in der ich Funktionen der instanziierten Klasse(n) aufrufe.
Ich so weit ist das klar.
Jetzt möchte ich auf C++-Seite eine Callback-Funktion, die in C# geschrieben ist, aufrufen.
Also quasi wie so etwas hier:
C#

Code: Alles auswählen

void MyCallbackFunction(int[] param)
{
 doSomethingWithParam(param);
}

static void Main(string[] args)
{
 MyDLLClass theClass = new MyDLLClass();
 theClass.setCallbackFunc(MyCallbackFunction);
 while(theClass.isRunning)
 {
  theClass.doSomething();
 }
}
Die Idee ist dahinter, dass dieses doSomething u.U. die Callback-Funktion aufrufen muss, und ihr ein array mit Integerwerten übergeben.

Ich hoffe mein Problem ist verständlich.
Was muss ich auf C# machen?
Was auf C++/ClI seite und was auf C++-Seite???

Hat jemand dazu erfahrung?
MasterQ? :)

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 13:38
von Krishty
  Marshal.GetFunctionPointerForDelegate(yourCSharpCallback)

Wir speichern keine direkten Callback-Zeiger, sondern legen sie in ein struct, und das muss ordentlich im Speicher gepinnt werden, damit es der GC nicht aufräumt, während es läuft. Ob das auch bei direkten Funktionszeigern nötig ist, kA. (Ich denke, schon.)

  GCHandle callbackPinned = GCHandle.Alloc(callbacks, GCHandleType.Pinned);
  IntPtr forCplusplus = callbackPinned.AddrOfPinnedObject();

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 13:44
von joggel
Okay, ich probiere das mal.

Und wie würde die Signatur in C++ ausehen müssen?
Also:
IntPtr forCplusplus = callbackPinned.AddrOfPinnedObject();
myCPlusPlusClass->setCSharpCallBackFunc(forCplusplus);
Wie soll dann die Signatur von setCSharpCallBackFunc aussehen?

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 14:00
von Krishty
Ist eigentlich beliebig, weil das Delegate das Hinbiegen von Calling Convention usw. übernimmt. Ich hab’s aber bisher nur mit static int function(IntPtr parameter) probiert.

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 14:06
von joggel
Ich bin mal wieder Noob in sachen Funktionspointer.
Ich meinte, wie sollte die Signatur auf C++-Seite aussehen?
So in etwa?
setCallBackFunc(void (*)(int**))
Ist das so korrekt?

Also, ich möchte ja ein Array mit ints übergeben...

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 14:55
von joggel
Oder anders gefragt:
Wenn ich den Pointer habe:

IntPtr forCplusplus = callbackPinned.AddrOfPinnedObject();

Wie rufe ich jetzt die Callback-Funktion aus C++ heraus auf?

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 15:49
von Krishty
Das kommt alles drauf an, wie die Signatur deiner C#-Funktion aussieht! Ist die Signatur z.B. static int foo(int), dann deklarierst du auf C++-Seite:

  using SetCallBackFunc = int __stdcall (int);
  auto toCallback = reinterpret_cast<SetCallBackFunc *>(der_intptr_der_aus_CSharp_kommt);
  auto returnValue = (*toCallback)(123);


__stdcall ist Standard-Calling Convention für C# & Co (weil COM und die WinAPI drauf aufsetzen), falls dein C++-Code aber unbedingt was anderes erwartet, kannst du das irgendwie via Attribut vor der C#-Funktion einstellen.

Ich empfehle auch stark, den Debugger so umzustellen, dass du C# und C++ gleichzeitig debuggen kannst. Das macht Visual C++ per default NICHT, man muss es erst irgendwo einstellen (Google weiß, wo). Ist ne Hilfe :)

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 15:57
von joggel
Alles klar.
Hilft mir schon weiter. Danke!!
Werd es morgen mal testen...jetzt ist mein Kopf zu leer (nach 8h dauerknobeln :| )^^

Das mit dem Umstellen werd ich mal googlen: wäre echt angenehmer...

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 16:18
von joggel
Compiler sagt "nein".

Code: Alles auswählen

void TheClass::setCallback(CallBackFunc^ theCallback)
{
	IntPtr ip = Marshal::GetFunctionPointerForDelegate(theCallback);

	array<int^>^ list;

	using SetCallBackFunc = int __stdcall (array<int^>^);
	auto toCallback = reinterpret_cast<SetCallBackFunc*>(ip.ToPointer());
	auto returnValue = (*toCallback)(list);
}
Ich wollte lediglich nur mal ausporieren, ob meine Funktion in C# aufgerufen wird, aber der Compiler haut mir hier die cast Zeile um die Ohren, mit folgender Begründung:

Fehler C2440 "Initialisierung": "SetCallBackFunc (__stdcall *)" kann nicht in "int (__stdcall *)(cli::array<System::Int32 ^,1> ^)" konvertiert werden

:?:

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 16:27
von Krishty
Ah, du bekommst direkt einen GCNew-Zeiger zu der Funktion? Dann kann man die bestimmt irgendwie direkt aufrufen, ohne Funktionszeiger-Hin-und-Her.

Da musst du aber wen anders fragen, mit CLI-Syntax kenne ich mich nicht aus …

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 16:31
von joggel
Ich möchte das ja dann an meinen unmanaged C++-Teil übergeben, und dort die Funktion aufrufen.
Also das Casten und so, werde ich bestimmt schon brauchen....

MasterQ weiß vlt etwas darüber, könnte ich mir vorstellen...

Re: C#-Callback-function in C++/Cli

Verfasst: 18.10.2016, 23:35
von xq
Klar hat der Meister da was:

Zur Managed-Seite, hier der Part in C#:

Code: Alles auswählen

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // Hier deine gewünschte Calling Convention
public delegate int MyCallback([MarshalAs(UnmanagedType.	LPArray)] int[] values);

public class Methods
{
    static MyCallback callback;
    public static IntPtr GetCallback() {
        if(callback == null)
            callback = MyImplementation; // Musst du bereit stellen
        return Marshal.GetFunctionPointerForDelegate(callback); // Liefere IntPtr auf Delegat zurück
    }
}
Dazu ein paar Anmerkungen:
  • Das Attribut am Delegattyp ist nicht zwangsweise relevant, ist aber wärmstens zu empfehlen, da du so Klarheit schaffst.
  • Marshal.GetFunctionPointerForDelegate ist der Weg, eine C#-"Methode" in einen Funktionspointer umzusetzen. Das ganze geht aber eben nur mit Delegaten, und darum:
  • Die Funktion liefert einen Pointer für den angegebenen Delegaten zurück. Sobald dieser vom Garbage Collector erfasst wird, ist der Pointer ungültig! Darum auch die Caching-Variable.
  • MarshalAs sorgt dafür dafür, dass das Array als C-Array übergeben wird (hier also int*). Falls die Länge des Arrays relevant sein sollte, musst du diese noch als zusätzlichen Parameter übergeben.
Nun der nicht verwaltete Teil:

Code: Alles auswählen

using MyCCallback = int __cdecl (*) (int * array);

void someFunc(IntPtr ^ptr)
{
    auto myCallback = static_cast<MyCCallback>(ptr.ToPointer()); // reinterpret_cast doch bitte NUR bei virtueller Vererbung ;)
    int data[] = { 1, 2, 3, 4 };
    int result = myCallback(data);
}
Bitte beachten: Nichts des ganzen wurde irgendwie getestet, kann also noch Fehler enthalten. Ich bin aber gerne für Rückfragen da ;)

Grüße
Felix

Re: C#-Callback-function in C++/Cli

Verfasst: 19.10.2016, 00:10
von biertrinker
Ohne die C++ Deklaration deines Funktionszeigers zu kennen und wie sich das Array zusammensetzt, kann man schlecht weiterhelfen.

Generell muss man bei so einem Reverse Interop allerdings ein paar Dinge beachten.
1. Die Callilng Convention muss passen
2. Die Parameter müssen stimmen (bei Arrays nicht ganz einfach)
3. Die Delegate Instanz muss erreichbar bleiben, da sie sonst vom Garbage Collector eingesammelt wird, während der zugehörige Funktionszeiger auf der C++ Seite noch in Gebrauch ist (Den Funktionszeiger, der von Marshal::GetFunctionPointerForDelegate zurückgegben wird, muss man allerdings nicht pinnen)

Wie die Deklaration des Delegates aussehen muss, kommt darauf an wie der Funktionszeiger in der deiner C++ Bibliothek deklariert ist und wie sich das Array zusammensetzt. Ich mach mal ein einfaches Beispiel: (Bin allerdings nicht so firm mit der C++/CLI Syntax, deshalb bitte nicht gleich copy und pasten)

Nehmen wir an, es gibt folgenden C++ Code

Code: Alles auswählen

// Im header
typedef void (*FunctionPointer)(int args[]);
void invoke(FunctionPointer func);

// In cpp Datei
void invoke(FunctionPointer func)
{
    int[] nums = {1, 2, 3, 4};
    func(nums);
}
Dann sieht das entsprechende Delegate für FunctionPointer in C++/CLI so aus:

Code: Alles auswählen

[UnmanagedFunctionPointer (CallingConvention::Cdecl)]
delegate void CallbackFunc ([MarshalAs(UnmanagedType::LPArray, SizeConst = 4)] array<int>^ args);
Die Calling Convention wird hier mit Cdecl angegeben, weil das die Standard-Konvention in C++ ist, wenn der Funktionszeiger nicht explizit als __stdcall deklariert wird. Managed Arrays haben eine vorgegebene Länge, allerdings weiß der Interop Marshaler nicht wie groß das C++ Array ist, weshalb man dies mit SizeConst angeben muss. Wenn die Größe erst zur Laufzeit bekannt ist, muss man zusätzlich einen Längen Parameter einführen und den SizeParamIndex angeben.

Auf der C++/CLI Seite lässt sich das folgendermaßen aufrufen

Code: Alles auswählen


// header der C++ Lib einbinden

void TheClass::invokeCallback(CallBackFunc^ theCallback)
{
    auto funcPtr = static_cast<FunctionPointer>(Marshal::GetFunctionPointerForDelegate(theCallback).ToPointer());
   
    // Aufruf der C++ Funktion
    invoke(funcPtr);
   
    // Sorgt dafür, dass das Delegate nicht vorzeitig vom Garbage Collector eingesammelt wird
    GC::KeepAlive(theCallback);
}
Wenn der Aufruf deines Callbacks asynchron ist oder deine C++ Bibliothek den Funktionszeiger über längere Zeit speichert, solltest du das Delegate in deiner Wrapper Klasse als Member speichern.

Re: C#-Callback-function in C++/Cli

Verfasst: 19.10.2016, 07:49
von joggel
MasterQ32 hat geschrieben:Klar hat der Meister da was:
Wußte ich doch :D
Ich werde mir das alles mal in Ruhe anschauen und probieren, auf jeden Fall sieht es danach aus was ich brauche. Danke!!
Falls ich noch Fragen habe, frage ich...oder ich weiß ja wo Du im WWW "rumhängst" => IRC ^^

@Biertrinker
Das sieht mir auch brauchbar aus. Was mich jedoch etwas stört ist, dass ich die Größe es Arrays eben nicht zur Laufzeit kenne, aber da hast Du ja einen Link dazu....muss ich mal schauen.
Wenn der Aufruf deines Callbacks asynchron ist oder deine C++ Bibliothek den Funktionszeiger über längere Zeit speichert, solltest du das Delegate in deiner Wrapper Klasse als Member speichern.
Ja, der Funktionszeiger wird über längeren Zeitraum gespeichert, also werd ich das wohl mal machen.

Jo, danke erstmal...ich würde mich bei weiteren fragen noch mal melden...


Nachtrag:
Es funktioniert!! :)

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 08:01
von joggel
Noch eine Frage: Habe ich etwas besonderes zu beachten, wenn ich statt ein array von ints, ein array von einer managed Klasse zurückgeben will?
Weil das mit dem int-array funktioniert, wenn ich jedoch eine array einer managed-Klasse über die callbackfunktion zurückgeben möchte, funktioniert es nicht...

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 09:41
von xq
Managed-Klassen gehen nicht.

Was geht, findest du hier: Marshaling Classes, Structures, and Unions

Du kannst Structs zurückgeben, auch Arrays von Structs. Aber alles nur mit "call-by-value"

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 09:44
von joggel
MasterQ32 hat geschrieben:Managed-Klassen gehen nicht.
Noooooooo :(

Okay, ich schaue mal wie ich das löse. Danke!

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 13:16
von xq
Was hast du denn genau vor?

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 13:52
von joggel
Fällt mir schwer das jetzt auch so wiederzugeben. Aber ich versuche es mal...

Ich möchte ja Teile einer C++-Bibliothek, oder besser gesagt, ein paar Funktionalitäten nach C# portieren.
Um genauer zu sein, geht es um einen OPC-Client, der ganz simpel nur Variablen von einem OPC-Server lesen oder schreiben soll.
Es gibt dabei auch eine Funktionalität, bei dem der Server den Clienten benachrichtigt wenn sich eine Variable ändert, und zwar übergibt man der C++-Bibliothek dann eine bestimmte abgeleitete Klasse als "Callback-Klasse". Sobald sich die bestimmten Variablen auf dem OPC-Server ändern, wird die (überschrieben) Funktion der "Callback-Klasse" aufgerufen.
Nun dachte ich mir eben, ich schreibe mir so eine Callback-Klasse und implementiere in die Aufruffunktion so, dass sie meine C#-Funktion aufruft...also mit einem Funktionspointer.
Die Aufruffunktion hat folgende Signatur:

Code: Alles auswählen

MyCallbckClass::onItemChange(CAtlMap<COPCItem *, OPCItemData *>& changes)
{
// hier dann der C#-Funktionsaufruf mit den Änderungen
}
Ich hatte jetzt auch die Klasse COPCItem als managed Klasse gewrapped, und die OPCItemData auch.
Aber eigentlich sind die relevanten Information NUR in OPCItemData. Was ich eigentlich auch als struct kapseln kann, was ich bestimmt auch bei Gelegenheit machen werde...

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 15:15
von xq
Ja, in dem fall wirst du wohl eher IN der onImteChange einfach Managed Code verwenden, und das Array übersetzen. Das dürfte dir Stress sparen

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 02.11.2016, 16:06
von joggel
That's the plan...

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 10.01.2017, 11:38
von joggel
Huhu,

ich muß noch mal das Thema rauskramen.

Ich bekomme es nämlich nicht hin :(

Mir kommt es so vor, als wird diese onItemChange-Funktion irgendwie nicht aufgerufen.

Habe ich das hier richtig gemacht?

Code: Alles auswählen

	[UnmanagedFunctionPointer(CallingConvention::Cdecl)]
	public delegate void CallbackFunc(array<OPCDAItemData^>^ args);
OPCDAItemData ist eine managed struktur.

HELP!!!

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 10.01.2017, 16:03
von xq
Könntest du mal ALLE relevanten Codestellen zu deinem Problem posten? Das würde einige Sachen massiv vereinfachen. Das oben ist ja irgendwie ein Mashup aus C# und C++/CLI, von daher ;)

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 10.01.2017, 16:30
von joggel
Jo, sorry.
Wollte ich gerade auch machen...aber du warst schneller.

Aaaaalso:
Hier in dieser Funktion scheint es immer einen Ausnahmefehler oder so zu geben.

Code: Alles auswählen

	void OnDataChange(COPCGroup &group, CAtlMap<COPCItem*, OPCItemData*> &changes)
	{
		array<OPCDAItemData^>^ arr = gcnew array<OPCDAItemData^>(changes.GetCount());
		POSITION pos;
		COPCItem* key;
		OPCItemData* value;
		pos = changes.GetStartPosition();

		int index(0);

		while ( pos != NULL )
		{
			key = changes.GetKeyAt(pos);
			value = changes.GetNextValue(pos);
			arr[index] = gcnew OPCDAItemData(value);
			++index;
		}

		mFunctionPtr(arr); // <= ich denke mal hier läuft irgend etwas schief. Wie der Name es schon sagt, ist das der Funktionspointer, dem ich das Array überegeb
}
Der Funktionspointer ist wie folgt deklariert:

Code: Alles auswählen

typedef void(*FunctionPointer)(array<OPCDAItemData^>^ arr);
Und hier ...(wie sagt man dazu?) die CallbackFunction-Deklaration

Code: Alles auswählen

	[UnmanagedFunctionPointer(CallingConvention::Cdecl)]
	public delegate void CallbackFunc(/*[MarshalAs(UnmanagedType::LPArray, SizeConst = 5)]*/ array<OPCDAItemData^>^ args);

So, ich habe jetzt erstmal Feierabend. Ich hoffe ich habe alles hier geschrieben.
Wünsch euch was :)

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 08:52
von xq
Zwei Sachen: Ist der Funktionspointer mFunctionPtr != nullptr und hast du deinen Delegaten, den du in den Funktionspointer marshalst, auch brav vom GC eingesammelt zu werden abgehalten (durch das Halten einer Referenz auf das Objekt)?

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 08:59
von joggel
Guten Morgen,

ob mFunctionPtr != nullptr ist, muß ich mal prüfen, denke aber schon das er nicht NULL ist.
und hast du deinen Delegaten, den du in den Funktionspointer marshalst, auch brav vom GC eingesammelt zu werden abgehalten (durch das Halten einer Referenz auf das Objekt)?
Denke habe ich auch. Mittels GC::KeepAlive(theCallback);

Hier mal der Code, also die Funktionsimplementation wo ich den Delegate überge

Code: Alles auswählen

void OPCDAGroup::enableAsynch(CallbackFunc^ theCallback)
{
	auto funcPtr = static_cast<FunctionPointer>(Marshal::GetFunctionPointerForDelegate(theCallback).ToPointer());

	if (mCallback == NULL)
	{
		mCallback = new MyCallBack();
		mCallback->setCallBack(funcPtr);
	}

	mGroup->enableAsynch(*mCallback);

	// Sorgt dafür, dass das Delegate nicht vorzeitig vom Garbage Collector eingesammelt wird
	GC::KeepAlive(theCallback);
}
Ich hoffe Du kannst das alles halbwegs nachvollziehen was ich hier schreibe; ist, glaube ich, alles ziemlich verwoben und so....
Vlt kann ich Dir ja auch mal das Projekt zuschicken oder so....natürlich nur, wenn du magst und ne freie minute hast...

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 09:54
von joggel
Nachtrag:
Diese Fehlermeldung schmeißt mir VS um die Ohren. Kann ich aber wenig damit anfangen
Fehler.png
Weitere Meldung:
Weitere Informationen finden Sie unter: Die Laufzeit hat einen schwerwiegenden Fehler entdeckt. Fehleradresse: "0x7399020d" in Thread "0x3c0". Fehlercode: 0xc0000005. Bei diesem Fehler könnte es sich um ein Problem in der CLR oder in den unsicheren oder nicht verifizierbaren Teilen des Benutzercodes handeln. Übliche Ursachen dieses Bugs sind Marshallerfehler für COM-Interop oder PInvoke, die den Stapel beschädigen können.

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 13:45
von xq
GC::KeepAlive(...) tut nicht das, was du erwartest. Das verhindert nur die Garbage Collection bis zum Call der Funktion. Du musst den theCallback in einer Membervariable eines lebendigen Objektes zwischenspeichern, bis du den Callback nicht mehr benötigst.

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 13:50
von joggel
Achso. Okay, werd ich dann mal ausprobieren.
Ich sag bescheid, ob es geklappt hat.

Re: [gelöst]C#-Callback-function in C++/Cli

Verfasst: 11.01.2017, 14:08
von Krishty
Reicht das denn? Könnte der GC nicht auch den Speicher kompaktieren, während die Funktion ausgeführt wird, und sie dabei an einen neuen Ort verschieben? Ich jedenfalls musste sie explizit pinnen (siehe erste Antwort).