c++ std::function => C# delegate



  • Hallo Leute,

    ich habe mal in C++/ C# cli wrapper geschrieben anhand von diversen snippts ausm i-net. Und es funktioniert auch.. allerdings weiß ich nicht ob es so gut is, wäre schön wenn Ihr mal drüber schauen könnten.. konstruktive Kritik höre ich gern;)

    Habe ein C++ Class mit RegisterCallback (std::function..) und ein Funktion FireCallback(...), welche dies dann aufruft. Das habe ich dann via CLI in die managed welt ge-wrapped, und zwar so:

    C++ Class "CallbackClass" functionen:

    void CallbackClass::RegisterCallback(std::function<bool(unsigned char*, int)> onReceive)
    {
    	_onReceive = onReceive;
    }
    
    bool CallbackClass::FireCallback(unsigned char *pBuffer, int length)
    {
            if (_onReceive) 
    	{
    		return _onReceive(pBuffer, length);
    	}
    	return false;
    }
    

    Cli Wrapper:

    
    public delegate bool OnReceive(array<unsigned char>^ buffer, int size);
    
    static std::function<bool(unsigned char*, int)> MakeCB(OnReceive^ f)
    {
    	gcroot<OnReceive^> f_wrap(f);
    	auto cb = [f_wrap](unsigned char* buffer, int len)
    	{
    		auto b = MakeManagedArray(buffer, len);
    		auto res = f_wrap->Invoke(b, len);
    		delete[] b;
    		return res;
    	};
    	return cb;
    }
    
    public ref class CallbackClass 
    {
    
    .....
    
            void FireCallback(array<unsigned char>^ buffer, int size)
    	{
    		if (buffer->Length == 0)
    			return;
    
    		GCHandle handle = GCHandle::Alloc(buffer, GCHandleType::Pinned);
    		try
    		{
    			unsigned char* ptr = (unsigned char*)(void*)handle.AddrOfPinnedObject();
    			this->m_Instance->FireCallback(ptr, size);
    		}
    		finally
    		{
    			handle.Free();
    		}
    	}
    	
            void RegisterCallback(OnReceive^ onReceive)
    	{
    		this->m_Instance->RegisterCallback(MakeCB(onReceive));
    	}
    };
    

    wenn ich den inhalt der "MakeCB" funktion direkt in "RegisterCallback" verwende, bekomme ich den fehler

    Error	C3923	'ClrLib::CallbackClass::RegisterCallback::<lambda_2dbcc89768a9421b25f1018f5984193c>': local class, struct or union definitions are not allowed in a member function of a managed class	ClrLib
    

    muss das also statisch machen.

    und hier c#

    static void testCallback()
                {
                ClrLib.OnReceive myCallback = (buffer, size) =>
                {
                    Console.WriteLine("TEST:" + buffer.Length);
                    return true;
                };
    
                try
                {
                    CallbackClass e = new CallbackClass();
    
                    e.RegisterCallback(myCallback);
                    
                    e.FireCallback(new Byte[] { 1, 2, 3, 4 }, 4);
    
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
                Console.Read();
            }
    

    so.. das ganze klappt auch.. aber ich weiß nich ob ich alles richtig gemach habe.. nich dass irgendwo memory leak etc. entstehen können.

    Danke für euere Mühen;)



  • @SoIntMan sagte in c++ std::function => C# delegate:

      auto b = MakeManagedArray(buffer, len);
      auto res = f_wrap->Invoke(b, len);
      delete[] b;
    

    Ich glaube das delete[] b da ist Quatsch. Erstmal ist ein managed Array ein Objekt und kein Array im C++ Sinn. Und weiters ist ein managed Array soweit ich weiss sowieso nicht disposable. delete[] b an der Stelle macht also vermutlich genau gar nix.



  • @hustbaer sagte in c++ std::function => C# delegate:

    delete[] b an der Stelle macht also vermutlich genau gar nix.

    Woher weiß es, dass es nichts machen muss? Sollte das nicht eher UB sein?



  • Woher weiß es, dass es nichts machen muss? Sollte das nicht eher UB sein?

    Was sollte es denn machen?

    Delete auf Reference Types ist in C++/CLI die Syntax um IDisposable::Dispose aufzurufen. Es ist aber auch erlaubt wenn der (statische) Typ IDisposable::Dispose gar nicht implementiert. Einerseits ist das praktisch in Templates, und andrerseits ist es praktisch wenn man einen Basisklassenzeiger hat, aber nicht weiss ob die konkrete Klasse vielleicht doch IDisposable implementiert. Der Compiler generiert einfach Code der checkt ob IDisposable implementiert ist, und wenn nicht, dann tut delete einfach nix.

    Von System.Array kann man allerdings nicht ableiten, und System.Array implementiert nicht IDisposable. Daher kann der Compiler wissen dass sich hinter der Array-Referenz kein Objekt verbirgt welches IDisposable implementiert - und kann den Dispose Aufruf gleich ganz weglassen. (Und tut das auch.)

    Warum die Array-Delete Syntax erlaubt ist weiss ich nicht. Unterschied in der generierten CIL machen es keinen: https://godbolt.org/z/j4fvWK

    Vielleicht ist auch das erlaubt damit das ganze besser mit C++ Templates zusammenspielt.



  • @hustbaer sagte in c++ std::function => C# delegate:

    auto b = MakeManagedArray(buffer, len);
    auto res = f_wrap->Invoke(b, len);
    delete[] b;

    ok nochmal zum Verständis: Diese code in cli ist quasi schon in managed context und da habe ich ein GC? der für mich das b aufräumt? Sorry c++/cli is Neuland für mich;O)



  • @SoIntMan MakeManagedArray gibt ein array<unsigned char>^ zurück, oder? Und type^ heisst du hast eine managed Referenz auf ein Objekt vom Typ type. Und solche Objekte räumt der GC weg.

    Das "Managed" in MakeManagedArray ist auch ein guter Hinweis darauf.

    Und ein delete auf so eine Referenz führt nicht dazu dass das Objekt irgendwie gelöscht wird. Es führt auch nicht dazu dass der Finalizer aufgerufen wird.
    Statt dessen wird geprüft ob das Objekt IDisposable implementiert, und wenn ja (dynamischer Typ!), dann wird IDisposable::Dispose aufgerufen.
    Das Objekt existiert danach weiterhin. Und irgendwann mal, nachdem alle Referenzen/Roots/... verschwunden sind und der GC mal wieder läuft*, wird das Objekt dann freigegeben.

    Bzw. falls das Objekt einen Finalizer hat und der Finalizer-Aufruf nicht unterdrückt wurde wird vor dem Freigeben erstmal der Finalizer aufgerufen. In dem Fall wird der Speicher dann erst in einer der nächsten GC Runden freigegeben.

    der für mich das b aufräumt?

    b ist bloss eine Variable deren Typ eine managed Referenz ist. b löst sich in Luft auf wenn der Scope verlassen wird. Und das Objekt auf welches b verweist wird dann irgendwann vom GC weggeräumt.

    Sorry c++/cli is Neuland für mich;O)

    Du solltest dich gut in das Thema einlesen. C++/CLI ist nicht gerade was wo man rumraten möchte.


    *: Es werden nicht bei jedem GC Lauf alle unerreichbaren Objekte freigegeben. Aber das zu erklären würde hier etwas den Rahmen sprengen. Stichworte: Generational Garbage Collector.



  • @hustbaer sagte in c++ std::function => C# delegate:

    @SoIntMan MakeManagedArray gibt ein array<unsigned char>^ zurück, oder? Und type^ heisst du hast eine managed Referenz auf ein Objekt vom Typ type. Und solche Objekte räumt der GC weg.

    Hallo hustbear, vielen Dank. das ist gut erklärt:)

    Jetzt habe ich noch eine weiter "Problemchen".

    Ich habe herausgefunden , dass bspw. die std::mutex /Thread etc. in meiner nativen lib nicht mit dem clr/cli wrapper compilieren. Clr mag wohl threading aus der std lib nicht!?

    Habe dann Pimpl-Idiom verwendet in meiner nativen lib um die implementierung mit meine mutex zu enkoppeln.. und siehe da, jetzt compiliert er..

    ABER ist das ne gute Idee? Ich denke die Clr lib wird einen Grund haben dass sie mutex nich mag?

    Vielen Dank und schönen Sonntag euch



  • @SoIntMan sagte in c++ std::function => C# delegate:

    Clr mag wohl threading aus der std lib nicht!?

    Nein. Das Problem ist dass std::mutex einen Destruktor hat. Und da vertragen sich die beiden Welten halt nicht (CLR auf der einen Seite und native C++ auf der anderen). Es passt weder die Semantik von Dispose noch die Semantik vom Finalizer so richtig. Daher ist es in C++/CLI nicht erlaubt Typen als Member zu verwenden die einen Destruktor haben.

    Habe dann Pimpl-Idiom verwendet in meiner nativen lib um die implementierung mit meine mutex zu enkoppeln.. und siehe da, jetzt compiliert er..

    Das ist der übliche Workaround. Du solltest dir zu dem Thema allerdings einiges durchlesen, und zwar:

    • Was ~Type in C++/CLI macht
    • Was %Type in C++/CLI macht
    • Was IDisposable::Dispose in .NET macht
    • Was ein Finalizer in .NET macht

    Und dann deine C++/CLI Klasse passend implementieren so dass keine Leaks entstehen etc.



  • @hustbaer sagte in c++ std::function => C# delegate:

    Nein. Das Problem ist dass std::mutex einen Destruktor hat. Und da vertragen sich die beiden Welten halt nicht (CLR auf der einen Seite und native C++ auf der anderen). Es passt weder die Semantik von Dispose noch die Semantik vom Finalizer so richtig. Daher ist es in C++/CLI nicht erlaubt Typen als Member zu verwenden die einen Destruktor haben.

    Habe dann Pimpl-Idiom verwendet in meiner nativen lib um die implementierung mit meine mutex zu enkoppeln.. und siehe da, jetzt compiliert er..

    Das ist der übliche Workaround. Du solltest dir zu dem Thema allerdings einiges durchlesen, und zwar:

    Perfekt Danke Dir, lies mich da mal durch:) Eines habe ich jetzt noch, und zwar folgendes:

    Ich kann meine c# app mit meinen cli /C++ lib auch super nutzen und es läuft alles so weit glatt.. Aber nach beenden der App (Console) ohne Fehler Exception etc. bleibt mir die "vshost.exe" als Prozess hängen, so das ich meinen Anwendung nicht neu starten kann, muss den vshost.exe hart killen... liegt das womöglich mit Memory-leaks etc. zusammen!?

    Ich weiß , sehr schwamming diese Frage , aber vll. hat ja jemand direkt ne Idee, was ich da falsch mache:)



  • @SoIntMan sagte in c++ std::function => C# delegate:

    Aber nach beenden der App (Console) ohne Fehler Exception etc. bleibt mir die "vshost.exe" als Prozess hängen, so das ich meinen Anwendung nicht neu starten kann, muss den vshost.exe hart killen... liegt das womöglich mit Memory-leaks etc. zusammen!?

    Da kann ich dir leider nicht helfen. Könnte mir schon vorstellen dass es mit Leaks (nicht Memory-Leaks, aber halt andere) zusammenhängen könnte. Könnte aber auch an was anderem liegen.

    Was du z.B. versuchen kannst ist schrittweise Funktionalität auszukommentieren, so lange bis nur mehr das leere C++/CLI "Gerüst" übrig ist (alle Funktionen leer implementiert). Und dann gucken bei welcher Änderung der Fehler verschwindet. Ist aber auch denkbar dass es mit C++/CLI selbst zusammenhängt.
    Nicht alles in Visual Studio ist immer und unbedingt so 100% fehlerfrei 😉



  • @hustbaer sagte in c++ std::function => C# delegate:

    Was du z.B. versuchen kannst ist schrittweise Funktionalität auszukommentieren, so lange bis nur mehr das leere C++/CLI "Gerüst" übrig ist (alle Funktionen leer implementiert). Und dann gucken bei welcher Änderung der Fehler verschwindet. Ist aber auch denkbar dass es mit C++/CLI selbst zusammenhängt.
    Nicht alles in Visual Studio ist immer und unbedingt so 100% fehlerfrei

    Guten Morgen,

    Danke hustbear.. du hast mir mehr als geholfen hier.. ja werde das mal machen.. aber das clr/cli konzept is irgendwie cool.. besser als PInvoke;)

    gibt es denn so ein cli/clr konzept für linux?;)


Log in to reply