Funktionsmarshalling generieren, um Funktionen und Parameter durch dumme Schnittstelle zu tunneln



  • Servus,

    ich habe mal wieder ein HST*-Problem:
    Und zwar muss ich zwischen zwei Modulen Funktionsaufrufe tunneln, über eine Schnittstelle, die vollkommen braindead ist. Ich weiß, dass es bessere Lösungen gibt, aber ich arbeite gerade in einem Brownfield-Projekt wo die oberste Direktive heißt "blos nicht zuviel Code anfassen".

    Seis drum. Hier das Problem:
    Die Schnittstelle zum Funktionsaustausch kann einen long und einen void* übertragen. Meine Aufgabe ist, darüber Funktionsaufrufe, insbesondere aber auch Konstruktion und Destruktion von Objekten zu tunneln.

    Der erste Ansatz ins Blaue sieht so aus:

    void sendMessage(long receiver, long input, void* param);
    void receiveMessageCallback(long sender, long input, void* param);
    
    // Klasse in Softwarekomponente A
    class Foo {
    public
        // Diese Klasse ist für Koordinaten
        Foo(string id, string name, double lat, double lon);
        bar(int);
    private:
     // nicht weiter wichtig
    };
    
    enum Functions{
        createFoo,
        createdFoo
        freeFoo,
        freedFoo,
        bar
    };
    
    // Callback-Funktion von Softwarekomponente A, wenn Komponente B sendMessage aufruft:
    void receiveMessageCallback(long sender, long input, void* param)
    {
            // herausfinden, welche Funktion gecallt werden soll
            switch(input)
            {
            case createFoo:
            {
                // Parametersatz herausprokeln.
                // ACHTUNG: Wi-der-lich !!
                std::string id = *static_cast<std::string*>(param);
                std::string name = *static_cast<std::string*>(param+sizeof(std::string*));
                double lat = *static_cast<double*>(param+2*sizeof(std::string*));
                double lon = *static_cast<double*>(param+2*sizeof(std::string*)+sizeof(double*));
                // Objekt aus Parametern generieren
                void* foo = new Foo(id, name, lat, lon);
    
                // Softwarekomponente B den Zeiger auf ihr Objekt geben
                sendMessage(sender, createdFoo, foo);
            }
                break;
            case freeFoo :
            {
                // parameter ist ein Foo-Objekt
                Foo* foo = static_cast<Foo*>(param);
                delete foo;
                // bescheid sagen, dass foo gelöscht ist.
                sendMessage(sender, freedFoo, 0);
            }
                break;
            default:
                std::abort();
            }
    

    Das ist der quick-and-dirty Ansatz.

    Ich kann von folgendem ausgehen:
    -Beide Komponenten laufen im gleichen virtuellen Speicherbereich
    -Die Wortbreite ist immer 32bit und der Byte-Order immer Intel

    Jetzt würde ich gerne den dummen Prozess des Parameter-Auseinander-Prokelns durch einen automatisierten Funktionsmarshalling-Generator mit Templates machen. Ich erinnere mich dunkel, sowas schonmal gesehen zu haben, und zwar unter Verwendung von boost::variant.

    Kann ich mir mit templates automatisch den Code zum Marshalling und Un-marshalling der Funktionsaufrufe generieren? boost::mpl?
    Bin für Anregungen dankbar.

    Als nächstes stellt sich die Frage, wie ich den expliziten Aufruf von freeXX vermeiden kann, da er ja am Ende doch irgendwo vergessen geht.
    Statt einem void* einen tr1::shared_ptr<> übergeben kommt mir da in den Sinn.

    Kennt jemand vorgefertigte Lösungen für sowas, ohne das man gleich einen CORBA-Server aufsetzen muss?

    Gruß,
    Philipp

    * HST: "Hooking shit together" http://www.theserverside.net/tt/talks/videos/PatHelland/interview.tss?bandwidth=56k



  • Ist sendMessage synchron? Wenn ja, dann könnte man u.U. Intrusive-Reference-Counting verwenden.

    Ich kann von folgendem ausgehen:
    -Beide Komponenten laufen im gleichen virtuellen Speicherbereich
    -Die Wortbreite ist immer 32bit und der Byte-Order immer Intel

    Kannst du dich auch darauf verlassen dass der selbe Compiler verwendet wird, mit der selbem STL-Version, selbes Service-Pack etc.?

    Wenn ja, dann ist der Code den du gepostet hast vermutlich OK, aber IMO viel zu kompliziert.

    Oder könnte es z.B. passieren dass ein Teil mit VC 6 und ein Teil mit VC 8 compiliert wird? Falls das der Fall sein sollte, dann kannst du std::string und dergleichen vergessen, da unterschiedliche Compilerversionen unterschiedliche Implementierungen von std::string haben. Dann müsstest du die Message-Daten irgendwie compilerunabhängig serialisieren und deserialisieren.

    Für den "selber Compiler" Fall:
    Du könntest du einfach eigene Message-Objekte definieren, und einen Zeiger auf diese übergeben:

    struct FooParameters
    {
        std::string id;
        std::string name;
        double lat;
        double lon;
    };
    
    class RefCountingBase : private noncopyable
    {
    public:
        virtual ~RefCountingBase();
        void AddRef();
        void Release();
    };
    
    class Foo : public RefCountingBase
    {
    public:
        explicit Foo(FooParameters const& parameters);
    // ...
    };
    
    void receiveMessageCallback(long sender, long input, void* param)
    {
            // herausfinden, welche Funktion gecallt werden soll
            switch(input)
            {
            case createFoo:
            {
                Foo* foo = new Foo(*static_cast<FooParameters const*>(param));
                foo->AddRef();
    
                // Softwarekomponente B den Zeiger auf ihr Objekt geben
                sendMessage(sender, createdFoo, foo);
    
                foo->Release(); // wenn "B" einen Referenz auf "foo" hält, muss "B" selbst "foo->AddRef()" machen
            }
    //...
    


  • hustbaer schrieb:

    Ist sendMessage synchron? Wenn ja, dann könnte man u.U. Intrusive-Reference-Counting verwenden.

    sendMessage ist in der tat synchron. Sendmessage tut nichts anderes als im anderen Modul receiveMessage synchron aufzurufen. Theoretisch könnte ich doch dann über param auch dann Rückgabewert zurückgeben, oder? Solange für den Aufrufenden klar ist, dass Eingabeparameter überschrieben werden, wäre das doch möglich?

    Was ist intrusive-reference-counting ?

    Kannst du dich auch darauf verlassen dass der selbe Compiler verwendet wird, mit der selbem STL-Version, selbes Service-Pack etc.?

    Wenn ja, dann ist der Code den du gepostet hast vermutlich OK, aber IMO viel zu kompliziert.

    Oder könnte es z.B. passieren dass ein Teil mit VC 6 und ein Teil mit VC 8 compiliert wird? Falls das der Fall sein sollte, dann kannst du std::string und dergleichen vergessen, da unterschiedliche Compilerversionen unterschiedliche Implementierungen von std::string haben. Dann müsstest du die Message-Daten irgendwie compilerunabhängig serialisieren und deserialisieren.

    Das ist sichergestellt. Der gesamte Code wird auf einem Rechner mit einem Compiler auf einen Rutsch durchkompiliert. Gleiche STL, libc usw...

    Für den "selber Compiler" Fall:
    Du könntest du einfach eigene Message-Objekte definieren, und einen Zeiger auf diese übergeben:
    ...

    Das sieht doch recht einfach aus, Danke!

    Philipp



  • hustbaer schrieb:

    foo->Release(); // wenn "B" einen Referenz auf "foo" hält, muss "B" selbst "foo->AddRef()" machen
    

    Sehe nicht ganz, wo da der Vorteil sein soll, wenn ich das "Add" und "Release" in B doch wieder von Hand machen muss.

    Daher: tr1::shared_ptr ?



  • Naja, shared_ptr wäre in dem Zusammenhang etwas fummelig. Du müsstest dann einen Zeiger auf eine shared_ptr<T> Instanz übergeben. Finde ich jetzt nicht so schön. Oder sonst könnte man noch enable_shared_from_this<T> verwenden, finde ich persönlich aber auch nicht so schön.

    Was Intrusive Reference Counting angeht: da gibt es in der Boost eine schöne Klasse namens intrusive_ptr die man dafür verwenden kann. Sind auch nur ein paar Zeilen Code, kann man also auch problemlos in eigene Projekte reinkopieren wenn man keine Abhängigkeit auf die Boost haben will.

    http://www.boost.org/doc/libs/1_46_0/libs/smart_ptr/intrusive_ptr.html

    Und das Schöne bei Intrusive Reference Counting ist dass man einen Smart-Pointer (boost::intrusive_ptr) jederzeit aus einem "rohen" Zeiger machen kann.

    Wenn es dir besser gefällt kannst du aber natürlich auch shared_ptr verwenden. Gehen tut es. Und falls du in dem Programm sowieso schon mit shared_ptr arbeitest ist das vermutlich sogar der bessere Weg.

    p.S.: Intrusive Reference Counting kann man begrenzt auch zwischen unterschiedlichen Compilern verwenden, siehe Windows/COM. Das ist der Grund warum ich gleich daran gedacht habe, ohne viel Gedanken auf shared_ptr zu verwenden. Das und die erwähnte fummelige Übergabe (Zeiger auf Zeiger, *schüttel* :)).

    Nochwas: Intrusive Reference Counting heisst es deswegen, weil das Referenz-Zählen in das Objekt "eindringt" (das Objekt muss ja selbst den Zähler implementieren). Wohingegen shared_ptr "non intrusive" ist - d.h. das Objekt selbst muss nix tun damit es mit shared_ptr zusammen funktioniert.



  • intrusive pointer ist aber eben genau das: Intrusive. Ich müsste jede einzelne Klasse davon erben lassen. Nix gut. Siehe oben, keinen Code anfassen, der funktioniert.

    Ich habe es jetzt so gelöst, dass die Parameter-structs als ersten Member immer ihren returnwert haben, was die Sache extrem komfortabel macht, da sendMessage() eben synchron ist:

    struct string_in_vecf_out {
        tr1::shared_ptr<std::vector<float> > return_target;
        std::string param1;
    };
    
    // aufrufender Code:
    string_in_vecf_out test;
    test.param1 = "Hallo Test";
    
    sendMessage(receiver, createTestObject, &test);
    
    tr1::shared_ptr<std::vector<float> > svf(test.return_target);
    std::cout <<  "vector size: " << svf->size() << std::endl;
    
    // aufgerufender Code:
    void receiveMessageCallback(long sender, long input, void* param)
    {
            switch(input)
            {
            case createTestObject:
                {
                    string_in_vecf_out* test = static_cast<string_in_vecf_out*>(param);
                    tr1::shared_ptr<std::vector<float> > p(new std::vector<float>(10));
                    id->return_target = p;
                }
                break;
            default:
                 std::abort();
            }
    }
    

    Das gewinnt zwar keinen Schönheitspreis, aber funktioniert.

    Jetzt zurück zur eigentlichen Frage:
    Das struct zu bauen, das könnte mir doch eigentlich der Compiler abnehmen.
    Im Grunde sowas wie (pseudocode)

    template <T return_type, Type_List arguments>
    struct Marshaller{
        return_type target;
        // irgendeine coole rekursion, die mir die argumenttypen auflistet
    };
    
    Marshaller<Foo> params;
    params.arg5 = "Hello";
    

    Wie gesagt, ich habe sowas schonmal gesehen, ich glaube unter Verwendung von boost::variant und boost::mpl.

    Wie könnte ich das realisieren?

    Philipp



  • intrusive pointer ist aber eb en genau das: Intrsuive. Ich müsste jede einzelne Klasse davon erben lassen. Nix gut. Siehe oben, keinen Code anfassen, der funktioniert.

    Ah, ok. Ich wusste nicht dass du da auch Objekte von Klassen übergeben willst, die bereits existieren.

    Jetzt zurück zur eigentlichen Frage:
    Das struct zu bauen, das könnte mir doch eigentlich der Compiler abnehmen.
    Im Grunde sowas wie (pseudocode)

    Dadurch sparst du dir die Definition einiger Hilfs-Structs.
    Allerdings bezahlst du es mit nicht-sprechenden Namen ("arg1", "arg2" etc.).

    Aber, eine ganz andere Idee: du könntest einfach Interfaces marshallen.

    So in der Art:

    #include <stdexcept>
    #include <string>
    #include <vector>
    #include <iostream>
    
    void sendMessage(long receiver, long input, void* param);
    void receiveMessageCallback(long sender, long input, void* param);
    
    enum Functions {
    	// ...
    	getInterface,
    };
    
    enum MyInterfaceID {
    	fooInterface,
    	barInterface,
    };
    
    struct InterfaceQuery
    {
    	explicit InterfaceQuery(MyInterfaceID iid) : interfaceId(iid), interface(0) {}
    
    	MyInterfaceID interfaceId;
    	void* interface;
    };
    
    template <class T>
    struct Invoker
    {
    	explicit Invoker(long receiver)
    	{
    		InterfaceQuery iq(T::iid);
    		sendMessage(receiver, getInterface, &iq);
    		m_interface = static_cast<T*>(iq.interface);
    		if (!m_interface)
    			throw std::runtime_error("blah");
    	}
    
    	T* operator -> ()
    	{
    		return m_interface;
    	}
    
    private:
    	T* m_interface;
    };
    
    // -----------------------------
    
    class Foo
    {
    public:
    	static MyInterfaceID const iid = fooInterface;
    	virtual void DoFoo(std::string a1, long a2, std::vector<char> const& a3) = 0;
    };
    
    class Bar
    {
    public:
    	static MyInterfaceID const iid = barInterface;
    	virtual void DoBar(int a1) = 0;
    };
    
    // -----------------------------
    
    class SomeObject : public Foo
    {
    public:
    	virtual void DoFoo(std::string a1, long a2, std::vector<char> const& a3)
    	{
    		std::cout << "DoFoo\n";
    	}
    };
    
    SomeObject g_someObject;
    
    void receiveMessageCallback(long sender, long input, void* param)
    {
    	switch (input)
    	{
    	case getInterface:
    		{
    			InterfaceQuery* iq = static_cast<InterfaceQuery*>(param);
    			switch (iq->interfaceId)
    			{
    				// kann man schön in ein Makro verpacken
    			case Foo::iid:
    				iq->interface = static_cast<Foo*>(&g_someObject); // der Cast nach Foo* ist wichtig, daher wäre es wirklich 
                                                                      // kein Fehler das in ein Makro zu verpacken
    
    				break;
    			default:
    				iq->interface = 0;
    				break;
    			}
    		}
    		break;
    
    	default:
    		abort();
    	}
    }
    
    int main() 
    {
    	Invoker<Foo>(1234)->DoFoo("blah", 1234, std::vector<char>());
    	return 0;
    }
    

    Weiss nicht ob das für dich brauchbar ist, da es vermutlich wieder erfordert dass man bestehende Klassen ändert.
    Allerdings müsste man nicht unbedingt vererben, man könnte die "Interfaces" auch als Member machen. Wobei das viel viel mehr Tippaufwand wäre, da man vermutlich Dutzendweise Forwarding-Funktionen schreiben müsste.

    Andrerseits ist der Impact auf bestehenden Code normalerweise minimal, wenn man einer Klasse eine zusätzliche Basisklasse spendiert, wenn man folgendes berücksichtigt:

    a) die neue Basisklasse hat nur virtuelle Funktionen und sonst keine Member
    b) die Namen der virtuellen Funktionen sind so gewählt, dass sie nicht mit anderen bestehenden (u.U. nicht virtuellen) Funktionen kollidieren können
    c) die neue Basisklasse wird NUR für den oben gezeigten Zweck verwendet
    d) die neue Basisklasse ist in einem eigenen Namespace für sich alleine definiert (um Probleme mit Argument-Dependent-Lookup zu vermeiden)

    Das ganze geht natürlich nur, wenn sendMessage() nicht z.B. irgendwelche Dinge synchronisiert, so dass ein Zugriff auf ein Objekt über den erhaltenen Interface-Zeiger nicht mehr OK wäre, weil sendMessage() schon Locks freigegeben hat.

    Und wenn die Zeiger auf die Objekte auch nach sendMessage noch gültig bleiben.

    Uswusf.



  • Ganz einfach wenn eine Dritte Bedingung erfüllt währe:

    -Beide Komponenten laufen im gleichen virtuellen Speicherbereich 
    -Die Wortbreite ist immer 32bit und der Byte-Order immer Intel 
    --> - Beide Module die Identische Code Basis benutzen
    

    Ist das so, dann könnte ich eine Lösung anbieten!

    Lichtlein



  • Puuuh, danke für die Mühe erstmal.

    Das Interface sieht garnicht blöd aus. Ich müsste tatsächlich nur unter alle betroffenen Klassen ein pure-virtual Interface drunterbasteln.

    Was mir aber nicht klar ist, wie in dem Zusammenhang überhaupt Objekte erzeugt werden sollen. In deinem Beispiel hält Komponente B ein globale SomeObject-Instanz.
    In meinem Fall muss aber Komponente A beliebig viele Instanzen erzeugen können, und auf jeder Instanz die Funktionen aufrufen.

    Der invoker bekäme dadurch einen weiteren Parameter, nämlich die Instanz, auf der überhaupt was invoked werden soll. Stellt sich die Frage, wen diese Instanzen gehören sollen.

    Philipp



  • Lichtlein schrieb:

    Ganz einfach wenn eine Dritte Bedingung erfüllt währe:

    --> - Beide Module die Identische Code Basis benutzen

    Du meinst, dass sie die selben Header inkludieren?
    Ja, das tun sie.

    Philipp



  • Nein ich dachte da an eine Binäre Datei in der mehr als 1 Core unterwegs sind.
    (Multiprozessor System).
    Es geht um den void Pointer, wenn es möglich ist diesen Pointer als Funktionspointer zu benutzen dann geht das was Du vorhast ganz einfach und sehr Flexibel.

    Lichtlein



  • Achso.

    Nein, by design ist das alles single-Threaded. Da kann kein anderer drin herumfummeln.
    Immer her mit der Idee!

    Philipp



  • PhilippM schrieb:

    Was mir aber nicht klar ist, wie in dem Zusammenhang überhaupt Objekte erzeugt werden sollen. In deinem Beispiel hält Komponente B ein globale SomeObject-Instanz.
    In meinem Fall muss aber Komponente A beliebig viele Instanzen erzeugen können, und auf jeder Instanz die Funktionen aufrufen.

    Der invoker bekäme dadurch einen weiteren Parameter, nämlich die Instanz, auf der überhaupt was invoked werden soll. Stellt sich die Frage, wen diese Instanzen gehören sollen.

    Dazu kann ich dir nun wirklich nichts sagen, da ich das Design von eurer sendMessage-Schnittstelle nicht kenne - ich hatte angenommen dass der "receiver" Wert in Wirklichkeit für ein Objekt steht, und nicht für ein "Modul".

    Ist aber auch egal, genau so wie in meinem Beispielcode kannst du ein "statisches" Interface mit Factory-Funktionen abfragen. Über diese Factory-Funktionen können dann direkt Objekte erzeugt werden - von denen muss man sich dann nichtmal die Interfaces über sendMessage holen, da man ja schon Zeiger hat.



  • Ich gebe Dir mal meine Idee, vielleicht hilft sie Dir ja weiter.
    Wie gesagt funktioniert das nur wenn Module A und Module B einen
    Funktionspointer an der gleichen Stelle haben und den gleichen Logischen Speicher benutzen.

    Also, Module A will, das Module B Type C anlegt und mit Werten x,y,z bestückt.

    Packen wir die Initial Werte in eine Struktur.

    struct TypeCInitValues
    {
        int x, y, z;
    };
    

    Als nächstes braucht man eine Fabrik Funktion die aus den Initialwerten Type C anlegt.

    TypeC* fabrikTypeC(const TypeCInitValue &init_values)
    {
        return new cTypeC(init_values.x, init_values.y, init_values.z);
    }
    

    Über Deine send und receive funktionen wollen wir nur einen Defenierten Type schicken.
    Der sieht so aus.

    struct SendData
    {
        virtual void receive();
    };
    

    Jetzt die Klasse die alles Verpackt und Sendet, und auch der empfänger der
    alles dann wieder entschlüsselt.

    struct SendHelper
    {
    
        template< typename data_tmp, typename fabik_funktion, typename generated_class>
        struct SendDataTmp : public SendData
        {
            virtual void receive()
            {
                generated_class *pGeneratetClass = (FabrikFunktion)(*pData);
    
                // MACH WAS MIT DER ERZEUGTEN CLASSE
            }
    
            data_tmp            *pData;
            fabrik_funktion     FabikFunktion;
        };
    
        template< typename init_data, typename generated_class >
        static void send(const INIT_DATA &init_data, generated_class* (*pfunc) (init_data &InitData))
        {
            typedef generated_class* (*tFunc) (init_data &InitData)     tFabikFunc;
    
            SendDataTmp<init_data, tFabikFunc, generated_class>  Sdt;
            Sdt.pData = &init_data;
            Sdt.FabrikFunktion = pfunc;
    
            sendMessage(0, 0, &Sdt)
        }
    
    };
    

    So und hier nun der Aufruf des ganzen.

    TypeCInitValues init_values;
    
    init_values.x = 1;
    init_values.y = 2;
    init_values.z = 3;
    
    SendHelper< TypeCInitValues >::send(init_values, fabrikTypeC);
    

    Habe ich jetzt aus den Kopf geschrieben, kann also sein das es nicht Compiliert.

    Hoffe Du erkennst was ich da gemeint habe, und das Du es benutzen kann oder wenigstens einen
    anregung.

    Lichtlein



  • Sieht mir jetzt ganz nach Visitor-Pattern aus.

    Ist auch nicht doof, bloss (ohne Lambda-Expressions) sehr umständlich, da man ziemlich viel tippen/kopieren muss wenn man unterschiedliche "MACH WAS MIT DER ERZEUGTEN CLASSE" (Klasse schreibt man mit K) braucht.

    Falls Lambda-Expressions zur Verfügung stehen finde ich die Idee allerdings sehr gut.



  • Hey, richtig klasse wieviele gute Ideen hier aufschlagen. Danke an alle Antworter 🙂

    Lambda-Funktionen habe ich keine. Noch ist kein boost im Projekt, und ich denke dabei bleibts auch.
    C++0x fällt auch flach, da der aktuellste Compiler auf dem Mac, den ich nutzen kann, 4.2 ist, und da ist noch nix mit C++0x.

    Aber:
    Ich habe an meinem Design nochmal ein bißchen gefeilt und siehe da - es gibt nur eine einzige Klasse, dere Methoden so getunnelt werden müssen, und - Überraschung - die ist auch noch ein Singleton.

    Daher passt Hustbaer's Lösung eigentlich wie angegossen. Das globale Objekt wird durch das Singleton ersetzt und voila! Funktioniert prächtig!

    Aber leider nur auf dem GCC.
    Wenn ich das durch Visual C++ 2010 jagen will, bekomme ich folgenden Fehler:

    enum Function {
        FSystem,
        // ...
    };
    
    enum FMSInterfaces {
        FMSSystemInterface,
        // ...
    };
    
    struct InterfaceQuery
    {
        explicit InterfaceQuery(FMSInterfaces iid) : interfaceId(iid), interface(0) {} // <--- das ist Zeile 25
    
        FMSInterfaces interfaceId;
        void* interface;  // <--- das ist Zeile 28
    };
    
    (28) : error C2332: 'struct': Fehlender Tagname
    (28) : error C2226: Syntaxfehler: Typ 'InterfaceQuery::<unnamed-tag>' nicht erwartet
    (28) : error C2238: Unerwartete(s) Token vor ';'
    (25) : error C2332: 'struct': Fehlender Tagname
    (25) : error C2011: '<unnamed-tag>': 'enum' Typneudefinition
    : Siehe Deklaration von '<unnamed-tag>'
    (25) : error C2059: Syntaxfehler: 'Typ'
    

    Und nu?
    GCC 4.4 und 4.5 kompilieren das wie gesagt prächitg.

    Gruß,
    Philipp

    EDITH hat festgestellt, dass wenn ich

    void* interface;
    

    in

    void* interface_;
    

    umbennene, es tadellos kompiliert.
    Hä?
    Ist "interface" bei msvc ein Schlüsselwort?

    Gruß,
    Philipp



  • PhilippM schrieb:

    Ist "interface" bei msvc ein Schlüsselwort?

    Nö, ist kein Schlüsselwort (obwohl das Syntax-Highlighting von VS 2010 es in Schlüsselwort-Farbe einfärbt).

    Aber irgendwo in den Untiefen der Windows-Header gibt's ein

    #define interface struct
    

    Hatte ich nicht dran gedacht. (Ich hatte den Code sogar mit MSVC geschrieben und probe-compiliert, bloss hatte ich da kein #include <windows.h> drinnen gehabt...)


Anmelden zum Antworten