Mit dem Hammer passt C++ durch C



  • Grüße!

    Ich bin gerade am herumspielen um irgendwie mal die möglichen Wege abzustecken, eine C-Basierte API nach außen anzubieten, damit sowohl C als auch C++-Code auf der anderen Seite mitmischen kann.
    Im Allgemeinen macht man das ja so, dass man bei den API-Funktionen immer noch so einen Detypisierten Wert mitgeben kann, der dann zum Beispiel einen Objektzeiger repräsentiert. Nun möchte man auf der internen Seite ja Zeiger auf Funktionen halten können, die dann der Kommunikationspartner auf der anderen Seite der C-API bekannt machen kann. Das kann aber selber ein C++-Jünger sein, oder aber echter C-Code. Das habe ich jetzt folgendermaßen versucht unter einen Hut zu bekommen:

    #include <iostream>
    using std::cout;
    
    extern "C" {
    
    	#define API _stdcall
    
    	typedef struct c_callback_t {
    		typedef int(API *FuncPtr)(void*);
    		FuncPtr func;
    	} c_callback;
    
    	int call_in_c(c_callback_t clb, void* parm)
    	{
    		return (*clb.func)(parm);
    	}
    }
    
    template<typename CCallbackType, typename Func, typename MemFunc >
    struct cpp_callback_impl;
    
    template<typename CCallbackType, typename Ret, typename UArg, typename... Args, typename MemFunc >
    struct cpp_callback_impl<CCallbackType, Ret(API*)(UArg, Args...), MemFunc>
    	: public CCallbackType
    {
    	cpp_callback_impl() {
    		func = &cpp_callback_impl::exec;
    	}
    
    	static Ret API exec(UArg uarg, Args&&... args) {
    		auto* cl = reinterpret_cast<typename MemFunc::Class*>(uarg);
    		return (cl->*MemFunc::Get())(std::forward<Args>(args)...);
    	}
    };
    
    template<typename CCallbackType, typename MemFunc >
    struct cpp_callback : public cpp_callback_impl<CCallbackType, typename CCallbackType::FuncPtr, MemFunc >
    {};
    
    template< typename ClassType, typename Prototype >
    struct MemFuncProto;
    
    template<typename ClassType, typename Ret, typename... Args >
    struct MemFuncProto<ClassType, Ret(Args...)> {
    	template<Ret(ClassType::*FuncPtr)(Args...)>
    	struct MemFunc {
    		using Class = ClassType;
    		static decltype(FuncPtr) Get() {
    			return FuncPtr;
    		}
    	};
    };
    
    struct TestClass {
    	int foo() {
    		std::cout<<"called Testclass::foo\n";
    		return 5;
    	}
    };
    
    int main()
    {
    	cpp_callback<c_callback, MemFuncProto<TestClass, int()>::MemFunc<&TestClass::foo>> TestClass_foo_callback;
    	TestClass a;
    	call_in_c(TestClass_foo_callback, (void*) &a);
    
    	return 0;
    }
    

    Jetzt könnte ich also intern ein c_callback und ein void* halten und der Aufruf könnte ein purer C-Aufruf oder ein durch template-Magie gewrappter C++-Aufruf werden.
    Jetzt meine Fragen: Geht das MemFuncProto-Zeugs da eventuell auch irgendwie "stringenter"? Was wären andere mögliche Ansätze, um solche Funktionalität umzusetzen, die vielleicht nicht in Template-Foo ausarten?



  • Was spricht dagegen die API in C zu schreiben und dann einfach einen entsprechenden Wrapper für C++ zu schreiben? So kann die C++ Version die Sprachfunktionen für die Implementierung komplett ausnutzen und muss keine weiteren Vorgaben einhalten.

    Ansonsten muss ich zugeben, dass ich mich mit dem Template Krams noch nie so ausführlich befasst habe. Mir ist daher nicht ganz klar, was da passiert. Jedenfalls muss ja eine Instanz der Klasse erzeugt werden (wo man keinen weiteren Einfluss drauf hat), die dann die Memberfunktion aufruft. Aber was soll der Sinn sein? In der Regel verwendet man hier statische Funktionen.



  • Achso, du willst die C++ Funktion auch in C über Wrapper aufrufen können? Dir ist aber schon klar, dass ein C Compiler keine Templates kennt? Und wenn er das kann, dann ist er ein C++ Compiler und kan mann direkt in C++ programmieren. 😕

    Mir erschließt sich das so gar nicht.



  • Man kann sehr wohl nach außen C Funktionen anbieten und intern in C++ schreiben.



  • Wozu soll das gut sein? C ist quasi irrelevant...



  • Kellerautomat schrieb:

    Wozu soll das gut sein? C ist quasi irrelevant...

    lol.



  • Kellerautomat schrieb:

    Wozu soll das gut sein? C ist quasi irrelevant...

    Bei vielen Programmiersprachen-Implementierungen gibt es eine Möglichkeit C Funktionen aufzurufen und zT ist es sogar möglich C Structs zu nutzen als wären sie eine Instanz eines zu Structs äquivalenten Sprachfeatures (Stichwort: foreign function interface).

    Ein C Interface anzubieten ist somit ein guter Anfang, wenn man seine Bibliothek für viele nutzbar machen möchte.



  • @EinerVonUns: Hast du dir eigentlich mal angeschaut, wie andere Bibliotheken Interfaces sowohl fuer C als auch fuer C++ anbieten? Wohl nicht. Auch ist es mir unklar, warum du es generisch, d.h. mit Templates, loesen musst. Hinzu kommt, dass ich echte Probleme mit deinem Deutsch habe. Ich verstehe die einzelnen Worte, nur nicht den Satz. Beispiel:

    Nun möchte man auf der internen Seite ja Zeiger auf Funktionen halten können, die dann der Kommunikationspartner auf der anderen Seite der C-API bekannt machen kann.

    Und normalerweise kann man eine C-API problemlos in C und C++ verwenden. Was willst du eigentlich uns mitteilen? Dein Post ist sehr nah am Trollen.



  • Du schreibst einfach eine C++ API wie es sich gehört. Wenn du fertig bist, klatscht du ein C Interface drüber. Fertig.

    Aus

    Klasse obj(1,2,3);
    obj.foo();
    

    wir einfach

    void* obj = create_klasse(1,2,3);
    foo(obj);
    

    in C

    PS:
    und noch ein lol @ Kellerautomat



  • Du schreibst einfach eine C++ API wie es sich gehört. Wenn du fertig bist, klatscht du ein C Interface drüber. Fertig.

    Wahlweise auch anders herum.



  • Also wenn ich nah am Trollen liege, dann tut's mir Leid... irgendwie sind dann aber andere hier schon des längeren fern auf der anderen Seite der Trollgrenze, ohne dass es dort von euch auch nur angemahnt würde...

    Mir geht es darum, dass ich keine C++-Compiler-Spezifische API extern anbieten möchte (Es gibt nunmal keine ABI). Die einzige Lösung, die mir dazu einfällt, ist es, dass auch in C++ geschriebene Erweiterungen sich durch die C-API durchquetschen müssen. Die C-API entsteht genau so, wie ihr es hier erwähnt: Sie wird als Wrapper um meinen existierenden C++-Code gelegt, der ohne Hinblick auf irgendwelche C-APIs entstand/entsteht.

    Mir geht es jetzt hauptsächlich darum, wie ich die C-API definiere, sodass eine C++-Erweiterung auf der anderen Seite möglichst wenig Aufwand hat, die C-API abermals zu wrappen. Und was ich an Helfer-Klassen für eine C++-Erweiterung dann anbieten kann. Eine solche Helferklasse wäre das beschriebene template, das für eine Memberfunktion einen C-Einstiegspunkt erstellt und das ganze fertig für die C-API verpackt (wenn auch mit ekliger Syntax, für deren Vereinfachung mir nur Makros einfallen).

    Nun möchte man auf der internen Seite ja Zeiger auf Funktionen halten können, die dann der Kommunikationspartner auf der anderen Seite der C-API bekannt machen kann.

    Das ist folgendermaßen gemeint: Die Host-Applikation soll Funktionszeiger zu Erweiterungs-Funktionen halten (Event-Handler oder ähnliches). Das wird in erster Linie ein C-Funktionszeiger und ein untypisierter Parameter sein. So kann eine C-Erweiterung und eine C++-Erweiterung zwischen Objektinstanzen, die beim Funktionsaufruf gemeint sind, unterscheiden (was wie ich auch erwähnte eine Standardtechnik beispielsweise in der Win32-API ist, ohne letztere jetzt qualitativ zu bewerten).

    Zuletzt habe ich auch im letzten Satz darauf hingewiesen, dass ich durchaus unvoreingenommen an guten Ansätzen anderer interessiert bin. Ihr wisst doch aber selber wahrscheinlich am besten, dass hier auf die Frage "Nennt mir ein Open-Source C++-Projekt" mit schönem und modernem C++ ungefähr 20 Antworten zu erwarten sind, die sich alle gegenseitig widersprechen. Ich würde gerne einige Beispiele durchschauen, aber die Suche nach "Gutes C++ Projekt C/C++-Plugin-API" führt zu nichts. Wenn ihr da etwas empfehlen könnt, dann _BITTE_, lasst mich daran teilhaben.



  • Wo genau ist das Problem ein Callback anzubieten?

    In C++ machst du das per functor und in der C API baust du dir aus dem reinen Funktionszeiger einen functor (uU ist dies sogar implizit Möglich). Du musst lediglich die Calling Convention beachten (die uU auch gleich ist).

    Nochmal:
    Du baust zuerst die interne API fertig, so wie sie sein soll. Und wenn sie fertig ist, dann klatscht du ein C Interface drüber.

    Auf welche konkreten Probleme bist du dabei denn gestoßen? Der C++ Code sah mir zu wirr aus um mich da durchzusehen. Ist diese riesige Komplexität wirklich notwendig? uU kennst du einfach nur std::function nicht?



  • Benutz Interfaces, dann braucht man kein C API -> C ist irrelevant



  • Kellerautomat schrieb:

    Benutz Interfaces, dann braucht man kein C API -> C ist irrelevant

    Du meinst sowas wie ISomething? Nun, bei Ableitung und Mehrfachvererbung gibt es Probleme ueber Librarygrenzen hinweg. Selbst erlebt. Dynamic dispatch virtueller Funktionen wird problematisch.

    Nennt mir ein Open-Source C++-Projekt" mit schönem und modernem C++

    Modernes C++ ist 2 Jahre alt, Projekte sind meist aelter. Projekte mit Callbacks oder C++/C-API: fltk und ZeroMQ.

    PS: Ich verstehe immer noch nicht dein Problem. Weiterhin glaube ich, dass dein Programm UB ist. Ein Methodenzeiger ist grundsaetzlich anders als ein "normaler" Funktionszeiger wie in C.
    http://stackoverflow.com/questions/12006854/why-the-size-of-a-pointer-to-a-function-is-different-from-the-size-of-a-pointer
    http://stackoverflow.com/questions/16062651/about-sizeof-of-a-class-member-function-pointer

    D.h. was du machst ist falsch. Bestenfalls so (Pseudocode):

    typedef struct c_callback_t
    { 
        WrapperFuncPtr func; // mit spezifischer signatur fuer callback
        void* obj;
    } c_callback; 
    
    class my_obj
    {
        int my_func(params ...);
    };
    
    int my_obj_wrapper(void* obj, params ...)
    {
       my_obj* pobj = (my_obj*)(obj);
       return pobj->my_func(params ...)
    }
    
    // using from C++ with call trough C-API
    
    my_obj yes_we_can;
    c_callback_t ywc_wrap;
    
    ywc_wrap.obj = yes_we_can;
    ywc_wrap.func = my_obj_wrapper; // no method pointer just normal function pointer
    

    Okay, ich sehe, dass du das so aehnlich machst, aber ... c_callback_t muss nicht das gleiche Layout wie irgendeine Klasse generiert aus dem Templatewust haben.

    auto* cl = reinterpret_cast<typename MemFunc::Class*>(uarg);
    

    Hier castest du einfach zu einem Memberfunctionpointer. Was wenn es gar kein Memberfunctionpointer ist?



  • Kellerautomat schrieb:

    Benutz Interfaces, dann braucht man kein C API -> C ist irrelevant

    lol.



  • Kellerautomat schrieb:

    Benutz Interfaces, dann braucht man kein C API -> C ist irrelevant

    Kannst du dich hier bitte raushalten wenn du nix intelligentes zu sagen hast?
    Danke.



  • Danke, die beiden Projekte werde ich mir anschauen.

    UB ist da doch eigentlich nichts? Was an die C-API weitergereicht wird, ist doch ein Zeiger auf eine statische Klassenfunktion mit definierter Calling-Convention? Gut, die statische Funktion wird erst durch ein Template erzeugt, aber ist doch an sich zu behandeln wie eine freie Funktion?

    Ich könnte selbstverständlich in der C++-Erweiterung auch eine einzelne "Aufruf-Routine" (oder meinetwegen statische Dispatcherroutine, oder wie man das nun benennen mag) haben, und der void* parameter der C-API würde dann als Zeiger auf ein std::function-Objekt genutzt, oder als Zeiger auf eine Struktur, die die methodenspezifische std::function enthält und einen this-Zeiger). Es führen da ja sicherlich 1000 Wege nach Rom und weil ich schwer absehen kann, was es nun im genauen bedeutet, wenn ich diesen oder jenen Weg einschlage, wende ich mich an euch, die ihr das schoneinmal gemacht habt und vielleicht auch einmal ein paar Fehler, aus denen man lernen könnte. Ich lerne derweilen erstmal aus meinen eigenen 😉



  • @knivil: Na klar, das reinterpret_cast muss natürlich davon ausgehen können, dass der void*-Zeiger durch das inverse reinterpret_cast entstand. Das irgendwie sicherer zu machen ist wohl kaum möglich, zumindest fällt mir da nichts ein, hast Du eine Idee? Da muss die Erweiterung der API trauen und die API muss der Erweiterung trauen, dass sie das schon irgendwie richtig macht.



  • Lol, ich merke was du eigentlich machst. Hier in einfach:

    #include <iostream> 
    using std::cout;
    
    typedef void(*callback_t)(void*);
    
    void call_cb(callback_t func, void* pobj) {
        func(pobj);
    }
    
    struct Test {
        void func() {
            std::cerr << "i am Test" << '\n';
        }
    };
    
    void wrap_test_func(void* pobj) {
        static_cast<Test*>(pobj)->func();
    }
    
    int main() {
        Test a;
        call_cb(wrap_test_func, &a);
    
        return 0;
    }
    

    Du kannsty gerne wrap_test_func in ein Template verwandeln, aber das was du machst wuerde ich aufgrund der Layoutzweifel als falsch empfinden. Und darueber hinaus: Einfach nur Overkill.

    Nachtrag mit wrapper als Templatate:

    template<typename T, typename void (T::*method)(void)>
    void wrapper(void* pobj)
    {
        (static_cast<T*>(pobj)->*method)();
    }
    
    int main()
    {
        Test a;
        call_cb(wrap_test_func, &a);
        call_cb(wrapper<Test, &Test::func>, &a);
    
        return 0;
    }
    


  • Ja, die Idee war, dass ich die Callbacks für die C-API in solcher Form definiere, dass ein template, das eine C-Wrapper-Funktion für eine Methode erzeugt, wenigstens die Signatur auf Kompatibilität checken kann.

    Und jetzt ist doch eine interessante Fragestellung aus diesem Beispiel entstanden: Ist das wegen etwaiger Regeln zu C/C++-Layouts wirklich UB, was ich da mache? Darf man von einer C-Struktur sozusagen ableiten und bedeutet dass, das der Teil des Objektes dem C-Alignment folgt? Ich slice ja den Rest weg (genau genommen ist da ja im Moment gar nichts anderes, den abgeleiteten Typ mal außen vor).
    Mit der Technik könnte ich halt einen Satz von Wrapper-Funktionen per Makro definieren, ohne immer einen kompletten Funktionskörper mit void*-cast schreiben zu müssen.
    Andererseits könnte ich den Körper auch direkt aus einem Makro stampfen. Müsste mal schauen, wie das aussähe.


Anmelden zum Antworten