Entwicklung der C-Wrapper-API für objektorientierten C ++ - Code

81

Ich möchte eine Reihe von C-APIs entwickeln, die unsere vorhandenen C ++ - APIs umschließen, um auf unsere Kernlogik zuzugreifen (geschrieben in objektorientiertem C ++). Dies wird im Wesentlichen eine Kleber-API sein, mit der unsere C ++ - Logik von anderen Sprachen verwendet werden kann. Welche guten Tutorials, Bücher oder Best Practices stellen die Konzepte vor, mit denen C um objektorientiertes C ++ gewickelt wird?

der Aktivist
quelle
4
Schauen Sie sich die Zeromq-Quelle an, um sich inspirieren zu lassen. Die Bibliothek ist derzeit in C ++ geschrieben und verfügt über C-Bindungen. zeromq.org
Hassan Syed
1
Verwandte (oder sogar ein Duplikat): Umschließen der C ++ - Klassen-API für den C-Verbrauch
Benutzer

Antworten:

69

Dies ist nicht allzu schwer von Hand zu tun, hängt jedoch von der Größe Ihrer Benutzeroberfläche ab. Die Fälle, in denen ich es getan habe, waren, die Verwendung unserer C ++ - Bibliothek aus reinem C-Code heraus zu ermöglichen, und daher war SWIG keine große Hilfe. (Nun, vielleicht kann SWIG verwendet werden, um dies zu tun, aber ich bin kein SWIG-Guru und es schien nicht trivial)

Am Ende haben wir nur Folgendes getan:

  1. Jedes Objekt wird in C einem undurchsichtigen Griff übergeben.
  2. Konstruktoren und Destruktoren sind in reine Funktionen eingeschlossen
  3. Mitgliedsfunktionen sind reine Funktionen.
  4. Andere Buildins werden nach Möglichkeit auf C-Äquivalente abgebildet.

Also eine Klasse wie diese (C ++ - Header)

class MyClass
{
  public:
  explicit MyClass( std::string & s );
  ~MyClass();
  int doSomething( int j );
}

Würde einer C-Schnittstelle wie dieser zugeordnet werden (C-Header):

struct HMyClass; // An opaque type that we'll use as a handle
typedef struct HMyClass HMyClass;
HMyClass * myStruct_create( const char * s );
void myStruct_destroy( HMyClass * v );
int myStruct_doSomething( HMyClass * v, int i );

Die Implementierung der Schnittstelle würde so aussehen (C ++ Quelle)

#include "MyClass.h"

extern "C" 
{
  HMyClass * myStruct_create( const char * s )
  {
    return reinterpret_cast<HMyClass*>( new MyClass( s ) );
  }
  void myStruct_destroy( HMyClass * v )
  {
    delete reinterpret_cast<MyClass*>(v);
  }
  int myStruct_doSomething( HMyClass * v, int i )
  {
    return reinterpret_cast<MyClass*>(v)->doSomething(i);
  }
}

Wir leiten unseren undurchsichtigen Griff von der ursprünglichen Klasse ab, um zu vermeiden, dass ein Casting erforderlich ist, und (Dies schien mit meinem aktuellen Complier nicht zu funktionieren). Wir müssen das Handle zu einer Struktur machen, da C keine Klassen unterstützt.

Das gibt uns also die grundlegende C-Schnittstelle. Wenn Sie ein vollständigeres Beispiel wünschen, das eine Möglichkeit zeigt, die Ausnahmebehandlung zu integrieren, können Sie meinen Code auf github ausprobieren: https://gist.github.com/mikeando/5394166

Der unterhaltsame Teil besteht nun darin, sicherzustellen, dass alle erforderlichen C ++ - Bibliotheken korrekt mit Ihrer größeren Bibliothek verknüpft sind. Für gcc (oder clang) bedeutet dies, dass nur die letzte Verbindungsphase mit g ++ durchgeführt wird.

Michael Anderson
quelle
11
Ich würde Ihnen empfehlen, etwas anderes als void zu verwenden, beispielsweise eine anonyme Struktur anstelle einer void * für das zurückgegebene Objekt. Dies kann eine Art Typensicherheit für die zurückgegebenen Griffe bieten. Weitere Informationen hierzu finden Sie unter stackoverflow.com/questions/839765/… .
Laserallan
3
Ich stimme Laserallan zu und habe meinen Code entsprechend überarbeitet
Michael Anderson
2
@ Mike Weller Das neue und Löschen im externen "C" -Block ist in Ordnung. Das externe "C" bewirkt nur das Mangeln des Namens. Der C-Compiler sieht diese Datei nie, nur den Header.
Michael Anderson
2
Ich habe auch ein typedef verpasst, das benötigt wird, um alles in C kompilieren zu können. Die seltsame typdef-Struktur Foo Foo; "hacken". Code wird aktualisiert
Michael Anderson
5
@ MichaelAnderson, es gibt zwei Tippfehler in Ihrer myStruct_destroyund myStruct_doSomethingFunktionen. Sollte sein reinterpret_cast<MyClass*>(v).
Firegurafiku
17

Ich denke, Michael Andersons Antwort ist auf dem richtigen Weg, aber mein Ansatz wäre anders. Sie müssen sich um eine zusätzliche Sache kümmern: Ausnahmen. Ausnahmen sind nicht Teil des C ABI, daher können Sie nicht zulassen, dass Ausnahmen jemals über den C ++ - Code hinausgeworfen werden. Ihr Header wird also so aussehen:

#ifdef __cplusplus
extern "C"
{
#endif
    void * myStruct_create( const char * s );
    void myStruct_destroy( void * v );
    int myStruct_doSomething( void * v, int i );
#ifdef __cplusplus
}
#endif

Die CPP-Datei Ihres Wrappers sieht folgendermaßen aus:

void * myStruct_create( const char * s ) {
    MyStruct * ms = NULL;
    try { /* The constructor for std::string may throw */
        ms = new MyStruct(s);
    } catch (...) {}
    return static_cast<void*>( ms );
}

void myStruct_destroy( void * v ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    delete ms;
}

int myStruct_doSomething( void * v, int i ) {
    MyStruct * ms = static_cast<MyStruct*>(v);
    int ret_value = -1; /* Assuming that a negative value means error */
    try {
        ret_value = ms->doSomething(i);
    } catch (...) {}
    return ret_value;
}

Noch besser: Wenn Sie wissen, dass alles, was Sie als einzelne Instanz von MyStruct benötigen, nicht das Risiko eingeht, dass ungültige Zeiger an Ihre API übergeben werden. Mach stattdessen so etwas:

static MyStruct * _ms = NULL;

int myStruct_create( const char * s ) {
    int ret_value = -1; /* error */
    try { /* The constructor for std::string may throw */
        _ms = new MyStruct(s);
        ret_value = 0; /* success */
    } catch (...) {}
    return ret_value;
}

void myStruct_destroy() {
    if (_ms != NULL) {
        delete _ms;
    }
}

int myStruct_doSomething( int i ) {
    int ret_value = -1; /* Assuming that a negative value means error */
    if (_ms != NULL) {
        try {
            ret_value = _ms->doSomething(i);
        } catch (...) {}
    }
    return ret_value;
}

Diese API ist viel sicherer.

Aber, wie Michael erwähnte, kann das Verknüpfen ziemlich schwierig werden.

Hoffe das hilft

figurassa
quelle
2
Weitere Informationen
Laserallan
2
Wenn ich weiß, dass meine C ++ - Bibliothek auch über eine C-API verfügt, kapsle ich einen API-Fehlercode int in meine Ausnahmebasisklasse. An der Wurfstelle ist es einfacher, die genaue Fehlerbedingung zu ermitteln und einen sehr spezifischen Fehlercode anzugeben. Die Try-Catch- "Wrapper" in den äußeren C-API-Funktionen müssen lediglich den Fehlercode abrufen und an den Aufrufer zurückgeben. Weitere Ausnahmen für Standardbibliotheken finden Sie unter dem Link von Laserallan.
Emile Cormier
2
catch (...) {} ist reines unverfälschtes Übel. Ich bedaure nur, dass ich nur einmal abstimmen kann.
Terry Mahaffey
2
@ Terry Mahaffey Ich stimme dir absolut zu, dass es böse ist. Das Beste ist, das zu tun, was Emile vorgeschlagen hat. Wenn Sie jedoch sicherstellen müssen, dass der umschlossene Code niemals ausgelöst wird, haben Sie keine andere Wahl, als einen Fang (...) am Ende aller anderen identifizierten Fänge zu setzen. Dies ist der Fall, weil die Bibliothek, die Sie umschließen, möglicherweise schlecht dokumentiert ist. Es gibt keine C ++ - Konstrukte, die Sie verwenden können, um zu erzwingen, dass nur eine Reihe von Ausnahmen ausgelöst werden dürfen. Was ist das kleinere von zwei Übeln? abfangen (...) oder einen Laufzeitabsturz riskieren, wenn der umschlossene Code versucht, den C-Aufrufer zu erreichen?
figurassa
1
catch (...) {std :: terminate (); } ist akzeptabel. catch (...) {} ist eine potenzielle Sicherheitslücke
Terry Mahaffey
10

Es ist nicht schwer, C ++ - Code für C verfügbar zu machen. Verwenden Sie einfach das Facade-Entwurfsmuster

Ich gehe davon aus, dass Ihr C ++ - Code in eine Bibliothek integriert ist. Sie müssen lediglich ein C-Modul in Ihrer C ++ - Bibliothek als Fassade für Ihre Bibliothek zusammen mit einer reinen C-Header-Datei erstellen. Das C-Modul ruft die relevanten C ++ - Funktionen auf

Sobald Sie dies getan haben, haben Ihre C-Anwendungen und Ihre Bibliothek vollen Zugriff auf die von Ihnen bereitgestellte C-API.

Hier ist zum Beispiel ein Beispiel für ein Fassadenmodul

#include <libInterface.h>
#include <objectedOrientedCppStuff.h>

int doObjectOrientedStuff(int *arg1, int arg2, char *arg3) {
      Object obj = ObjectFactory->makeCppObj(arg3); // doing object oriented stuff here
      obj->doStuff(arg2);
      return obj->doMoreStuff(arg1);
   }

Sie stellen diese C-Funktion dann als Ihre API zur Verfügung und können sie frei als C-Bibliothek verwenden, ohne sich Sorgen machen zu müssen

// file name "libIntrface.h"
extern int doObjectOrientedStuff(int *, int, char*);

Natürlich ist dies ein erfundenes Beispiel, aber dies ist der einfachste Weg, eine C ++ - Bibliothek für C verfügbar zu machen

hhafez
quelle
Hallo @hhafez, hast du ein einfaches Hallo-Welt-Beispiel? Eins mit Saiten?
Jason Foglia
für einen Nicht-CPP-Typ ist das sehr schön
Nicklas Avén
6

Ich würde denken, dass Sie in der Lage sein könnten, einige Ideen zur Richtung zu bekommen und / oder SWIG direkt zu nutzen . Ich würde denken, dass ein Blick auf einige Beispiele Ihnen zumindest eine Vorstellung davon geben würde, welche Dinge zu beachten sind, wenn Sie eine API in eine andere einbinden. Die Übung könnte von Vorteil sein.

SWIG ist ein Softwareentwicklungstool, das in C und C ++ geschriebene Programme mit einer Vielzahl von Programmiersprachen auf hoher Ebene verbindet. SWIG wird mit verschiedenen Arten von Sprachen verwendet, einschließlich gängiger Skriptsprachen wie Perl, PHP, Python, Tcl und Ruby. Die Liste der unterstützten Sprachen enthält auch Nicht-Skriptsprachen wie C #, Common Lisp (CLISP, Allegro CL, CFFI, UFFI), Java, Lua, Modula-3, OCAML, Octave und R. Auch mehrere interpretierte und kompilierte Schema-Implementierungen ( Guile, MzScheme, Chicken) werden unterstützt. SWIG wird am häufigsten zum Erstellen von interpretierten oder kompilierten Programmierumgebungen auf hoher Ebene, von Benutzeroberflächen und als Tool zum Testen und Prototyping von C / C ++ - Software verwendet. SWIG kann seinen Analysebaum auch in Form von XML- und Lisp-S-Ausdrücken exportieren. SWIG kann frei verwendet, verteilt werden,

harschware
quelle
2
SWIG ist gerade über Kill, wenn alles, was er tun möchte, ist, eine C ++ - Bibliothek von C.
hhafez
1
Das ist eine Meinung und enthält kein wirklich nützliches Feedback. SWIG würde helfen, wenn der ursprüngliche Code lautet: Schnelle Änderung, Es gibt keine C ++ - Ressourcen, um ihn zu verwalten, und nur C-Ressourcen verfügbar, und wenn der Entwickler die C-API-Generierung automatisieren möchte. Dies sind häufige und sicherlich gültige Gründe für die Verwendung von SWIG.
user1363990
5

Ersetzen Sie einfach das Konzept eines Objekts durch ein void *(in C-orientierten Bibliotheken häufig als undurchsichtiger Typ bezeichnet) und verwenden Sie alles, was Sie aus C ++ wissen, wieder.

Hassan Syed
quelle
2

Ich denke, SWIG ist die beste Antwort ... es vermeidet nicht nur eine Neuerfindung des Rades, sondern ist auch zuverlässig und fördert eine Kontinuität in der Entwicklung, anstatt das Problem zu lösen.

Hochfrequenzprobleme müssen durch eine langfristige Lösung angegangen werden.

Danbo
quelle